拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从零搭建本地HTML文件工作台:Curio思路与Python实践

如果你和我一样平时会积攒大量 HTML 文件一定遇到过这种场景从书签、文档、演示项目里复制出来的单页 HTML随手扔进文件夹过两周再打开时已经分不清哪些能直接运行、哪些还依赖外部资源、哪些只是写到一半的半成品。最近 Show HN 上出现了一个叫 Curio 的项目定位很朴素a place for HTML files一个专门收纳 HTML 文件的地方。这个点子看起来简单实际踩中了很多前端学习者和入门玩家的痛点。本文会从 Curio 的设计理念出发分析 HTML 文件本地管理的难点然后带你从零搭建一个“Curio 风格”的本地 HTML 文件工作台。你不仅能管理 HTML 文件还能在浏览器里直接预览、搜索、新建和删除文件并且理解本地文件预览中的编码、安全和 iframe 等关键问题。1. 从 Show HN 说起Curio 是什么1.1 它解决了什么痛点很多开发者可能觉得HTML 文件不就是双击用浏览器打开吗为什么要专门做一个工具来“存放”它们实际并非如此。当你手里积累了几百个 HTML 文件时会遇到下面这些问题常见场景具体痛点收藏的网页源码文件散落各处没有统一入口想找到某个历史 Demo 很困难课程作业、实验代码需要反复展示给老师或同事但每次都要定位文件路径再双击网页制作练习一个 HTML 文件可能依赖同目录下的 CSS、JS、图片没有目录级别预览入口半成品页面忘记哪个版本可以正常显示需要逐个打开验证本地转换需求想把 HTML 里的表格转成 Excel/WPS 表格或转成 Markdown需要快速访问源文件Curio 的核心理念就是把这些杂乱的 HTML 文件收纳到一个“书架”里让它们有索引、可检索、可预览。它不是复杂的 Web 框架也不是内容管理系统而是一个轻量、专注的本地 HTML 文件管理器。1.2 适合谁使用Curio 这类工具适合下面几类人群前端初学者保存了大量练习页面和网页制作案例。经常做单页 HTML Demo 的开发者。需要向别人快速展示页面效果的产品、设计和开发人员。喜欢保存网页源码、然后又喜欢二次修改的手工党。如果你只是偶尔写一个 HTML 文件那用系统自带文件管理器就够了。但如果你是“收藏型开发者”Curio 的工作流能明显提高效率。1.3 它不是万能的需要说明的是Curio 的定位是“存放 HTML 文件”的地方而不是一个完整的前端开发环境。它通常不负责写代码、编译 Sass、打包构建。真实项目里我建议把它和 VS Code、WebStorm 这类编辑器配合使用用编辑器写代码用 Curio 统一管理和预览成品文件。2. HTML 文件本地管理的底层问题为什么我们不直接双击 HTML 文件而是要通过 HTTP 服务来预览这里有几个很容易踩坑的点。2.1 file:// 与 http:// 的差异很多人在本地双击 HTML 文件时浏览器地址栏显示的是file:///C:/Users/.../index.html。这种“文件协议”有很严格的限制现代浏览器不允许file://页面通过fetch读取其他本地文件。很多 ES Module 模块在这种协议下无法正常加载。部分浏览器插件不会在file://页面生效。当 HTML 文件依赖相对路径的 CSS、JS 时如果路径中带有中文或空格很容易解析失败。而通过http://127.0.0.1:8080访问时浏览器把它当成一个真正的网站允许fetch、允许模块加载、允许相对路径解析整体表现更接近线上环境。2.2 iframe 预览的坑Curio 这类工具通常采用“左侧文件列表 右侧 iframe 预览”的布局。但 iframe 预览并不是万能的如果 HTML 页面设置了X-Frame-Options: DENY或 CSP 中的frame-ancestorsiframe 会加载失败。如果 HTML 页面应用了sandbox属性其中的脚本和表单可能被限制。本地文件如果包含跨域资源可能需要允许混合内容或配置代理。因此在实现预览时我们要给 iframe 保留一个“新窗口打开”的逃生口并且提供 sandbox 开关。2.3 字符集和编码国内很多 HTML 文件都存在编码问题。一个标准的 HTML 页面应该像下面这样!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title页面标题/title /head body !-- 页面内容 -- /body /html如果文件没有写meta charsetUTF-8浏览器只能靠猜测解析编码中文很容易变成乱码。Curio 这类管理工具在扫描文件时最好顺带提取title并暴露给前端这样列表页可以显示语义化标题而不是一堆demo-2024-xxx.html这种文件名。3. 环境准备与项目结构下面我们动手实现一个简化版 Curio。我的目标是用一个 Python 脚本提供静态文件服务和 JSON API用一个 HTML 文件作为前端工作台。3.1 运行环境本文示例不需要数据库不需要 Node 环境。你只需要Python 3.10 及以上版本。一个现代浏览器推荐 Chrome 或 Edge。可选VS Code 或者 Ubuntu 系统下的任意 HTML 编辑器Vim、Gedit 等都可以。Python 版本主要影响类型注解和functools.partial等写法。如果你使用的是 Python 3.8示例代码也基本兼容但建议直接使用 Python 3.10。3.2 工作区目录结构我们在一个空目录中创建项目curio-workspace/ ├── curio_server.py # Python 后端服务 └── workspaces/ # HTML 文件实际存放目录 ├── index.html # Curio 前端页面 └── projects/ # 示例子目录 ├── heart.html └── table-demo.htmlcurio_server.py会默认读取同目录下的workspaces/并提供两类能力静态文件服务让浏览器能访问工作区里的 HTML、CSS、JS 文件。JSON API扫描并返回 HTML 文件列表支持新建和删除文件。4. 编写后端文件扫描与静态服务我们先写核心的后端脚本。4.1 创建后端文件在curio-server/目录下新建curio_server.py代码如下# 文件路径curio-server/curio_server.py import json import os import re import functools from http.server import HTTPServer, SimpleHTTPRequestHandler from urllib.parse import urlparse # 工作区目录和脚本同级的 workspaces BASE_DIR os.path.join(os.path.dirname(os.path.abspath(__file__)), workspaces) def extract_title(file_path): 从 HTML 文件中提取 title 标签内容 try: with open(file_path, r, encodingutf-8, errorsignore) as f: content f.read(4096) m re.search(rtitle[^]*(.*?)/title, content, re.I | re.S) if m: return m.group(1).strip() except Exception: pass return None def scan_html_files(root): 递归扫描目录下所有 .html / .htm 文件 items [] skip_dirs {.git, node_modules, venv, __pycache__} for dirpath, dirnames, filenames in os.walk(root): # 跳过常见无关目录避免索引很慢 dirnames[:] [d for d in dirnames if d not in skip_dirs] html_files [f for f in filenames if f.lower().endswith((.html, .htm))] if not html_files: continue for filename in html_files: abs_path os.path.join(dirpath, filename) rel_path os.path.relpath(abs_path, root).replace(os.sep, /) stat os.stat(abs_path) items.append({ path: rel_path, name: filename, title: extract_title(abs_path), size: stat.st_size, mtime: int(stat.st_mtime) }) # 按最新修改时间倒序 items.sort(keylambda x: x[mtime], reverseTrue) return items def safe_path(rel_path): 校验相对路径是否存在于工作区内部防止路径穿越 if not rel_path: return None if os.path.isabs(rel_path): return None full os.path.realpath(os.path.join(BASE_DIR, rel_path)) try: if os.path.commonpath([full, BASE_DIR]) ! BASE_DIR: return None except ValueError: return None return full这里有一个关键点safe_path用来防止“路径穿越”。如果攻击者给接口传一个../../etc/passwdos.path.join会把路径拼到工作区外面。通过os.path.realpath和os.path.commonpath校验可以确保访问路径始终在工作区内部。4.2 HTTP API 与静态文件服务接着我们定义请求处理器# 继续追加到 curio_server.py class CurioHandler(SimpleHTTPRequestHandler): 自定义请求处理优先处理 API 路由其余走静态文件服务 def send_json(self, obj, status200): body json.dumps(obj, ensure_asciiFalse).encode(utf-8) self.send_response(status) self.send_header(Content-Type, application/json; charsetutf-8) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) def read_json_body(self): length int(self.headers.get(Content-Length, 0)) if length 0 or length 1024 * 1024: return None raw self.rfile.read(length) try: return json.loads(raw.decode(utf-8)) except Exception: return None def do_GET(self): parsed urlparse(self.path) if parsed.path /api/files: data scan_html_files(BASE_DIR) self.send_json(data) return if parsed.path.startswith(/api/): self.send_json({error: Not Found}, 404) return # 其他请求交给静态文件服务 super().do_GET() def do_POST(self): parsed urlparse(self.path) if parsed.path /api/create: data self.read_json_body() if not data: self.send_json({error: 请求体不是合法 JSON}, 400) return name (data.get(name) or ).strip() # 过滤掉 Windows/Unix 路径中的非法字符 safe_name re.sub(r[\\/:*?|\x00-\x1f], _, name) if not safe_name: self.send_json({error: 文件名不能为空}, 400) return if not safe_name.lower().endswith(.html): safe_name .html full_path os.path.join(BASE_DIR, safe_name) if os.path.exists(full_path): self.send_json({error: 文件已存在}, 409) return template ( !DOCTYPE html\n html langzh-CN\n head\n meta charsetUTF-8\n meta nameviewport contentwidthdevice-width, initial-scale1.0\n f title{safe_name}/title\n style\n \n /style\n /head\n body\n f h1{safe_name}/h1\n script\n \n \/script\n /body\n /html\n ) with open(full_path, w, encodingutf-8) as f: f.write(template) self.send_json({ok: True, path: safe_name}) return if parsed.path /api/delete: data self.read_json_body() if not data: self.send_json({error: 请求体不是合法 JSON}, 400) return rel_path data.get(path) full_path safe_path(rel_path) if rel_path else None if not full_path or not os.path.isfile(full_path): self.send_json({error: 文件不存在}, 404) return # 示例脚本演示用直接删除文件。 # 实际项目建议先移到回收站目录而不是直接 os.remove。 os.remove(full_path) self.send_json({ok: True}) return self.send_json({error: Not Found}, 404) def main(): os.makedirs(BASE_DIR, exist_okTrue) # 通过 functools.partial 把工作区目录传给 SimpleHTTPRequestHandler handler functools.partial(CurioHandler, directoryBASE_DIR) server HTTPServer((127.0.
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门