上传一个 ZIP,得到一个网站。
解压在浏览器里完成,Cloudflare 只管存储和分发 —— 所以免费额度就够,成本 $0。
- 💸 零成本 — Workers + R2 + D1 全部落在免费额度内,附逐项核算
- 🧊 浏览器端解压 — ZIP 在访客上传者的浏览器里用 Web Worker 流式解开,绕开 Workers 免费版 10ms CPU / 128MB 内存 / 50 子请求三重限制(这是服务端解压方案必然超时的原因)
- 🌐 子域名隔离 — 每个项目一个
<项目名>.你的域名,项目内绝对路径互不串扰,不改写 HTML - ⚡ 部署即生效 — 边缘缓存键包含部署版本号,重新部署立即全局生效,不用等 TTL
- 🖥️ 自带入口页 — 根域名自动渲染项目收藏页(搜索 + 统计 + 后台入口),管理后台由 Worker 自己托管,不用 Cloudflare Pages
- 🛡️ 内置防护 — 路径穿越拦截、系统垃圾文件过滤、HMAC 签名会话、常量时间口令比较
- ✅ 60 项端到端测试 —
npm run smoke用真实 ZIP 走完登录、上传、分发、覆盖部署、删除全流程
| 环节 | 由谁负责 |
|---|---|
| 解压 ZIP | 你的浏览器(public/unzip-worker.js,用 fflate 流式解压) |
| 分片上传 | 浏览器按「≤40 个文件 / ≤20MB」一批推给 /api/projects/<名字>/files |
| 存文件 | Worker 写入 R2,键为 <项目名>/<路径> |
| 存元数据 | D1(入口文件名、文件数、体积、部署版本) |
| 对外入口页 | 根域名上由 Worker 渲染项目列表 + 后台入口(src/landing.js) |
| 分发访问 | 同一个 Worker 按 Host 取项目名 → 读 R2 → 按 deploy_id 做边缘缓存 |
三个 Host 各司其职,只需要两条路由(由部署脚本从 ROOT_DOMAIN 自动生成):
<你的域名> -> 对外入口页(项目列表 + 后台入口) 路由:<域名>/*
admin.<你的域名> -> 管理后台界面 + /api/* 路由:*.<域名>/*
<项目名>.<你的域名> -> 该项目在 R2 里的静态文件 ─────────┘
通配路由不匹配根域名本身,所以根域名那条必须单独写。两条都是 zone route,不会撞上"每个域名 100 个自定义域名"的上限。
为什么选子域名而不是路径:子域名天然隔离,项目里的 /style.css 这类绝对路径不会串到别的项目,不需要改写 HTML,也不用担心 <base>。
为什么不用 Cloudflare Pages:Pages 不支持通配自定义域名,且有 100 个项目 / 20000 文件 / 每月 500 次构建的限制;R2 方案没有这些天花板。
本地跑起来不需要 Cloudflare 账号 —— Miniflare 会在本地模拟 R2 和 D1。
git clone <你的仓库地址> easel && cd easel
npm install # 安装依赖,并把 fflate 拷入 public/vendor/
cp .dev.vars.example .dev.vars
npm run db:local # 给本地 D1 建表
npm run dev # http://127.0.0.1:8787| 看什么 | 地址 |
|---|---|
| 访客视角的入口页 | http://127.0.0.1:8787/ 或 http://localhost:8787/ |
| 管理后台 | http://admin.localhost:8787/ |
| 某个项目 | http://<项目名>.localhost:8787/ |
本地口令在 .dev.vars 里(模板默认 dev-password-change-me)。Chrome 会把 *.localhost 解析到 127.0.0.1,三个地址都能直接访问。
跑测试:
npm run smoke # 另开一个终端跑着 npm run dev,然后用真实 ZIP 走 60 项断言Tip
改了服务端代码后如果页面没变化,是浏览器缓存 —— 入口页 60 秒、资源 10 分钟。加个 ?v=1 或 ?fresh=1 立刻看到最新内容。
从零开始、全新账号大约 15 分钟。建议按顺序走,每步验证过再进行下一步,出问题能立刻定位到环节。
已经熟悉 wrangler 的话,最短路径(点开)
npm install
npx wrangler login
npx wrangler r2 bucket create <全局唯一的桶名>
npx wrangler d1 create html-hosting
# ↓ 把上面两条命令的输出填进 wrangler.jsonc 的 bucket_name / database_id,并改 ROOT_DOMAIN
npm run db:remote
npx wrangler secret put ADMIN_PASSWORD
npx wrangler secret put AUTH_SECRET
npm run deploy
# ↓ 还差 Dashboard 里的两条 DNS 记录(* 和 @,都要开橙云)0.1 Cloudflare 账号 — 没有就去 dash.cloudflare.com 注册,免费套餐足够。
0.2 域名必须已经托管在 Cloudflare — 判断方法:Dashboard 首页能看到这个域名,且状态是「有效 / Active」。域名在别处注册的话,需要先把 NS 改成 Cloudflare 给的那两个地址,等生效(通常几分钟,最长 24 小时)。
为什么必须:本项目靠 Workers 路由(*.<域名>/*)工作,只有托管在 Cloudflare 的域名才能配路由,也才能拿到 *.<域名> 的免费证书。
0.3 开通 R2 订阅
Important
R2 即使只用免费额度,也要先在账号里走一次开通流程,否则第 1 步创建桶会直接失败。 路径:Dashboard → Storage & databases → R2 → Overview → 按提示完成 checkout。 这一步会要求添加支付方式;免费额度内(10GB 存储 + 每月 100 万次 A 类 + 1000 万次 B 类操作)不产生费用。
0.4 Node 环境与 wrangler 登录
node --version # 需要 18 以上(本机验证用的是 v26)
npm install # 装 fflate / wrangler,并把 fflate 拷进 public/vendor/
npx wrangler login # 会打开浏览器要求授权
npx wrangler whoami # 确认已登录,并核对是你要用的那个账号wrangler whoami 会列出账号名和账号 ID。如果你有多个 Cloudflare 账号,这一步一定要看清,后面所有资源都会建在它下面。未登录时它会提示 You are not authenticated。
npx wrangler r2 bucket create <你的桶名>桶名规则(Cloudflare 强制):
- 全球唯一 —— 所有 Cloudflare 用户共用一个命名空间,
html-hosting、static、files这类名字基本都被占了 - 3–63 个字符,只能用小写字母、数字、连字符,首尾必须是字母或数字
- 建议加个随机后缀,例如
html-hosting-a7f3c1
成功后把桶名填进 wrangler.jsonc:
验证:
npx wrangler r2 bucket list # 应该能看到刚建的桶Warning
别用 --update-config 让 wrangler 自动改配置:它会直接重写 wrangler.jsonc、把里面的注释丢掉。手工粘贴上面这一处更稳妥。
npx wrangler d1 create html-hosting(数据库名可以改,但改了三处都要跟着改:wrangler.jsonc 的 database_name,以及 package.json 里 db:local / db:remote 两条脚本中的 html-hosting。)
命令会输出一段可直接粘贴的配置片段,里面有一个 database_id(形如 a1b2c3d4-0000-1111-2222-333344445555)。复制那个 id,填进 wrangler.jsonc:
"d1_databases": [
{
"binding": "DB",
"database_name": "html-hosting",
"database_id": "a1b2c3d4-0000-1111-2222-333344445555" // ← 替换掉 REPLACE_WITH_YOUR_D1_DATABASE_ID
}
],验证:
npx wrangler d1 listNote
database_name 是给人看的名字(账号内唯一),database_id 是 UUID(全局标识)。要填的是 id。
npm run db:remote实际执行的是 wrangler d1 execute html-hosting --remote --file=schema.sql -y,只做一件事:在线上 D1 里建 projects 表和索引。语句是 CREATE TABLE IF NOT EXISTS,重复执行也安全。
预期输出里有 2 commands executed successfully。验证表确实建好了:
npx wrangler d1 execute html-hosting --remote --command "SELECT name FROM sqlite_master WHERE type='table'"应该看到 projects。
Warning
后台报 no such table: projects → 这一步没做,或者做成了 --local。本地开发用 npm run db:local,两者互不影响。
打开 wrangler.jsonc 的 vars:
| 变量 | 默认值 | 要不要改 | 说明 |
|---|---|---|---|
ROOT_DOMAIN |
example.com |
你的域名。两条路由(<域名>/* 和 *.<域名>/*)都由它自动推导,不要另外去配 routes |
|
ADMIN_SUBDOMAIN |
admin |
一般不用 | 后台所在子域名,即 admin.<域名> |
SITE_TITLE |
我的网页收藏 |
建议改 | 入口页大标题,也用作浏览器标签页标题 |
SITE_DESCRIPTION |
一句话 | 建议改 | 入口页标题下方的描述,同时作为页面 SEO 描述 |
LANDING_AT_APEX |
true |
视情况 | 见下方说明 |
改完大概长这样:
"vars": {
"ROOT_DOMAIN": "mydomain.dev",
"ADMIN_SUBDOMAIN": "admin",
"SITE_TITLE": "我的网页收藏",
"SITE_DESCRIPTION": "这里托管着我收集的静态页面,点开任意一个都能直接浏览。",
"LANDING_AT_APEX": true
}LANDING_AT_APEX 什么时候改成 false:入口页要放在根域名,就需要一条 <域名>/* 路由,而这条路由会把根域名上原本的站点顶掉。如果你的根域名上已经挂着博客或其他服务,改成 false —— 部署脚本就只生成通配路由,入口页改用 www.<域名> 或任意子域名访问。
Dashboard → 选中你的域名 → 左侧 DNS → Records → Add record,加两条:
| 类型 | 名称(Name) | IPv4 地址 | 代理状态 |
|---|---|---|---|
A |
* |
192.0.2.1 |
Proxied(橙云) |
A |
@ |
192.0.2.1 |
Proxied(橙云) |
三个要点:
- IP 为什么可以随便填:这两个主机名的请求会被 Worker 路由截获,永远不会回源,IP 是什么根本到不了。
192.0.2.1是 RFC 5737 保留给文档示例的地址,不会撞上真实主机。 - 为什么必须开橙云:只有被代理(橙云)的请求才会经过 Cloudflare 边缘,才能命中 Worker 路由、才能用上
*.<域名>的免费证书。灰云(DNS only)会让请求直接去连那个不存在的 IP,结果是连接失败。 @那条只在入口页放根域名时才需要;LANDING_AT_APEX=false就跳过。根域名若已有记录(比如指向你的博客),不要动它。
关于 SSL/TLS 模式:这两个主机名都由 Worker 直接处理、不回源,所以 Flexible / Full 在这里没有区别,不用改。
验证 DNS 已生效(可能需要等一两分钟):
nslookup anything.mydomain.dev返回 Cloudflare 的 IP(而不是 192.0.2.1)就说明代理生效了。
Note
通配记录是多级的,但免费版 Universal SSL 只覆盖一层子域名 —— 所以 a.mydomain.dev 能用,a.b.mydomain.dev 会证书错误。本项目按一层设计。
这两个是 Worker Secret,存在 Cloudflare 侧、不属于代码库。它们不是 vars —— vars 会明文写进配置文件,只能放非敏感内容。
npx wrangler secret put ADMIN_PASSWORD回车后提示 Enter a secret value:,粘贴你的后台口令再回车(输入过程不回显)。这个口令就是你登录 admin.<域名> 用的那个。
# 先生成一个 64 位随机串
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# 再把上面输出的串粘贴进去
npx wrangler secret put AUTH_SECRETAUTH_SECRET 用于给会话 Cookie 签名,和登录口令是两回事,泄露它等同于泄露后台。
验证:
npx wrangler secret list # 应该列出 ADMIN_PASSWORD 和 AUTH_SECRETWarning
不要在命令行参数里传口令(比如 echo "xxx" | wrangler secret put ...),那样会留在 shell 历史里。交互式粘贴最安全。
要点:
- Secret 存在 Cloudflare 侧,后续
wrangler deploy不会覆盖或清掉它们,只有再执行一次secret put才会改。 - 首次部署前必须先设好,否则后台会提示"服务端未设置 ADMIN_PASSWORD"。
- 本地开发读的是
.dev.vars里的同名变量,与线上互不影响。
npm run deploy这条命令串了三步,任何一步失败都会停下:
node scripts/preflight.mjs—— 检查占位值是否已改(ROOT_DOMAIN、database_id)、public/vendor/fflate.js是否存在。不通过会打印具体要改哪一行然后退出。node scripts/build-config.mjs—— 从ROOT_DOMAIN生成wrangler.deploy.jsonc,把两条路由写进去。这是生成物,已在.gitignore里,不要手改。wrangler deploy -c wrangler.deploy.jsonc—— 真正上传。
预期输出(关键几行;Total Upload 那行是实测值,路由列表的文字以 wrangler 版本为准):
✔ 配置检查通过(域名 mydomain.dev,桶 html-hosting-a7f3c1)
✔ 已生成 wrangler.deploy.jsonc(路由 mydomain.dev/* + *.mydomain.dev/*)
Total Upload: 53.58 KiB / gzip: 16.69 KiB
Deployed html-hosting triggers
mydomain.dev/*
*.mydomain.dev/*
只要最后能看到你期望的两条路由(<域名>/* 和 *.<域名>/*),就说明路由挂上了。
常见报错(前三条来自 preflight,它会先打印 ✖ 部署前检查未通过:,再逐条列出来):
| 报错 | 原因 |
|---|---|
vars.ROOT_DOMAIN 还是占位值 "example.com" |
第 4 步没做 |
d1_databases[0].database_id 还没填 |
第 2 步的 id 没填 |
✖ 缺少 public/vendor/fflate.js,请先执行:npm run vendor |
没跑 npm install(或 npm run vendor) |
Could not find zone "mydomain.dev" |
域名不在当前账号下,或 wrangler whoami 是另一个账号 |
A worker with the name "html-hosting" already exists |
账号里已有同名 Worker:删掉它,或改 wrangler.jsonc 的 name |
| 提示超出账号 Worker 数量上限 | 免费套餐上限 100 个 Worker |
查部署状态:
npx wrangler deployments list # 最近 10 次部署
npx wrangler deployments status # 当前线上生效的版本按顺序来,每步确认了再往下,出问题能立刻定位。
8.1 入口页 —— 浏览器打开 https://你的域名/
预期:入口页(大标题 + 空状态"还没有托管任何页面" + 右上角「管理后台」)。
| 现象 | 原因 |
|---|---|
| 404 | 根域名的 @ DNS 记录没加或没开代理;或 LANDING_AT_APEX=false |
| 显示的是别的站点 | 根域名本来就有记录,路由没生效 —— 检查 @ 指向了哪里 |
ERR_TOO_MANY_REDIRECTS |
别处配了重定向规则(如 Always Use HTTPS + 页面规则冲突) |
8.2 后台 —— 点右上角「管理后台」,或直接开 https://admin.你的域名/
预期:登录界面;输入第 6 步的口令后进入控制台。
| 现象 | 原因 |
|---|---|
| 提示"服务端未设置 ADMIN_PASSWORD" | 第 6 步的 secret 没设,或设完没重新部署 |
| 提示"口令不正确" | 口令不对(大小写、首尾空格都算) |
| 页面一直转圈 | 开浏览器控制台看 /api/session 报什么错 |
8.3 上传第一个项目
在后台拖一个 ZIP 上去(手边没有就随意压一个含 index.html 的文件夹)。观察四件事:
- 选完文件立刻出现探测结果:入口文件、文件数量、解压后体积
- 点「开始上传」,进度条走到 100%
- 日志出现
✓ 上线:https://<项目名>.你的域名/ - 项目列表里出现卡片
8.4 打开项目 —— 点卡片上的「打开」
预期:新标签页打开 https://<项目名>.你的域名/,显示你上传的页面。
| 现象 | 原因 |
|---|---|
证书错误 / ERR_SSL_VERSION_OR_CIPHER_MISMATCH |
通配证书还在签发(Universal SSL 通常几分钟,最长 24 小时);或子域名超过一层 |
| 404「项目不存在」 | 子域名和项目名对不上,核对卡片上的地址 |
| 一直显示"正在部署" | 上次上传中断了,重新部署一次即可覆盖 |
8.5 回入口页 —— 刷新 https://你的域名/
预期:新项目出现在列表里。入口页给浏览器缓存了 60 秒,等一会儿或加 ?v=1 立刻看到。
口令只有一层保护。代码里有常量时间比较和单 isolate 内的尝试节流,但没有全局限流。免费套餐含 1 条 WAF 速率限制规则,加一条就够:
Dashboard → 你的域名 → Security → WAF → Rate limiting rules → Create rule
- 匹配条件:
URI Path等于/api/login - 阈值:例如「60 秒内超过 10 次请求」
- 动作:Block,持续 60 秒
| 你想做什么 | 怎么做 |
|---|---|
改代码(src/、public/) |
npm run deploy |
| 改入口页文案 | 改 wrangler.jsonc 的 SITE_TITLE / SITE_DESCRIPTION → npm run deploy(访客侧最多 60 秒后生效) |
| 换域名 | 改 ROOT_DOMAIN → 给新域名加两条 DNS 记录 → npm run deploy。旧域名的路由会被移除,但旧 DNS 记录要自己删 |
| 改后台口令 | npx wrangler secret put ADMIN_PASSWORD(不需要重新部署) |
| 增删项目、重新部署项目 | 全在后台界面操作,不涉及 npm run deploy |
| 回滚到上一个版本 | npx wrangler rollback,或 Dashboard → Workers → 该 Worker → Deployments → 选历史版本 Rollback |
npx wrangler delete # 删 Worker(路由一并移除)
npx wrangler r2 bucket delete <桶名> # 删桶(需先清空里面的对象)
npx wrangler d1 delete html-hosting # 删数据库最后回 Dashboard 的 DNS 页面手动删掉那两条 A 记录。
对访客(https://你的域名/):
- 列出所有托管中的页面,点卡片直接打开
- 顶部有搜索框(按
/聚焦,Esc清空),项目多了也能快速找到 - 右上角「管理后台」是给你用的入口
对你(https://admin.<你的域名>/):
- 上传新项目:拖入 ZIP 或点选文件。项目名会自动从文件名推导,可以改。界面会先显示探测结果(入口文件、文件数量、解压后体积、是否剥掉了顶层目录、忽略了几个系统垃圾文件),确认后再上传。
- 项目卡片:打开 / 复制链接 / 重新部署(覆盖) / 删除。
- 顶栏「查看入口页」可以直接跳到访客看到的那一页。
- 上传进度和每一步的结果都写在日志区,失败会自动重试 3 次(指数退避)。
自动处理的事情:
- 剥离 ZIP 里常见的单层顶层目录(
mysite/index.html→index.html) - 丢弃
__MACOSX/、.DS_Store、Thumbs.db、desktop.ini、._*、.git/等垃圾 - 丢弃带
..或绝对路径的条目(防路径穿越) - 入口文件探测:优先根目录
index.html,其次浅层的home/default/main.html - 覆盖部署时按
deploy_id清理上一版残留的文件 - 入口页只列出部署完成的项目,上传中的不会露出来
URL 行为:
/→ 入口文件/about→ 先试about/index.html,再试about.html- 找不到时,如果项目里有
404.html就返回它 - 加
?fresh=1绕过所有缓存,用来核对线上内容
cp .dev.vars.example .dev.vars # 本地密钥模板,已在 .gitignore 里
npm run db:local # 给本地 D1 建表
npm run dev # http://127.0.0.1:8787自动化冒烟测试(另开一个终端跑着 npm run dev):
npm run smoke会用真实 ZIP 走完整流程,覆盖 60 项断言:鉴权、项目名校验、路径穿越拦截、分片上传、入口文件校验、子域名分发、Content-Type、ETag 304、无扩展名回退、404、对外入口页(含「admin 子域名不被入口页顶掉」这条回归)、覆盖部署与残留清理、边缘缓存命中/绕过、删除。改完代码跑一遍,比手点可靠。
Warning
不要把 routes 写回 wrangler.jsonc。 只要配置里存在 routes,wrangler dev 的本地服务器就会把所有请求的 Host 重写成路由域名(实测会把任何 Host 变成 example.com),本地完全没法测试子域名分发。这就是路由被挪到部署时生成的原因,详见 scripts/build-config.mjs 顶部注释。
以 200 个项目、每个 1000 个文件共 5MB 计:
| 项目 | 用量 | 免费额度 | 余量 |
|---|---|---|---|
| R2 存储 | 1 GB | 10 GB / 月 | 10% |
| R2 A 类操作(上传、列出、删除) | 约 20 万 / 月 | 100 万 / 月 | 20% |
| R2 B 类操作(读取) | 边缘缓存未命中才计 | 1000 万 / 月 | 充裕 |
| D1 读 | 每个资源请求 1 次(用于取 deploy_id),入口页每次 1 次 |
500 万 / 天 | 充裕 |
| D1 写 | 每次部署 2~3 次 | 10 万 / 天 | 充裕 |
| Workers 请求 | 每访问一个资源算 1 次,入口页每次 1 次 | 10 万 / 天 | 最先撞墙 |
最需要留意的一条:边缘缓存能减少 R2 读取、降低延迟,但不能减少 Worker 的请求计数 —— 一个含 20 个资源的页面每天被访问 5000 次就吃满 10 万/天,之后当天返回错误页。个人收藏夹的流量下够用;如果要分享给较多人,唯一的现实付费点是升级 Workers Paid($5/月,含 1000 万请求/月)。
另外 R2 免费额度只适用于 Standard 存储,出口流量免费。
- 浏览器缓存有延迟:边缘缓存按
deploy_id换键,重新部署在边缘是立刻全局生效的;但访客浏览器自己的副本要等max-age过期(HTML 60 秒,其他资源 10 分钟)。给资源换文件名可以立刻绕过。想调这两个值改src/mime.js的cacheControlFor。入口页同理缓存 60 秒(改文案后加?v=1可立刻看到)。 - 单次上传体积上限:单文件 ≤50MB、单批 ≤30MB、单项目解压后 ≤300MB、≤5000 个文件。这些是浏览器内存和 Workers 请求体(100MB)限制推导出来的,常量写在
public/unzip-worker.js和src/api.js顶部。 - 口令暴力破解防护有限:只有常量时间比较 + 单 isolate 内的尝试节流,没有全局限流。建议按上面的说明加一条 WAF 速率限制规则。
- 项目名必须是合法 DNS 标签:小写字母数字与连字符,≤40 字符,不能连续连字符,且不能在保留名单里(
www/admin/api/mail等,见src/util.js)。 www.<域名>会 302 到根域名,不参与托管。- 会话 Cookie 有效期 7 天,过期后重新登录。
刻意不做的事:不改写项目内的绝对路径引用(子域名方案天然隔离,这正是选它的原因)、不做多用户注册、不做访问统计、不生成缩略图、默认不保留原始 ZIP。
为什么解压放在浏览器,而不是 Worker 里? Workers 免费版每次调用只有 10ms CPU、128MB 内存、50 个子请求。服务端解压稍大的包必然触发 1102 超时,而且往 R2 写每个文件都算一次子请求,50 个文件就是上限。挪到浏览器后这三条限制全部消失,R2 本身也没有 25MiB 单文件限制。
忘记后台口令怎么办?
npx wrangler secret put ADMIN_PASSWORD 重新设一个即可,立即生效,不需要重新部署。
为什么我的项目里 /style.css 加载不到别的项目的内容?
不会发生 —— 每个项目在独立子域名上,浏览器同源策略和键前缀双重隔离。这正是选子域名方案的原因。
上传失败提示"单批请求过大"? 多半是单个文件超过 50MB,或某一批总字节超过 30MB。日志区会写明是哪一批。超大视频建议放 R2 单独托管或外链。
其余部署问题见部署上线各步的报错表,或下面的排查表:
| 现象 | 原因 |
|---|---|
访问 <项目>.<域名> 显示的是管理后台 |
vars.ROOT_DOMAIN 和实际域名不一致,或通配 DNS 记录没建/没开代理 |
| 根域名打开是 404,或还是原来那个站点 | 根域名的 @ A 记录(名称 @)没建或没开代理,或者 LANDING_AT_APEX 是 false 而根域名没走 Worker |
改了 SITE_TITLE 但入口页没变 |
入口页给浏览器缓存了 60 秒,等一会儿或加 ?v=1 强制刷新 |
| 某个项目没出现在入口页 | 它还在上传中(status != ready),部署完成才会出现 |
npm run deploy 报 zone not found |
域名不在当前登录的账号下,或 wrangler login 登错了账号 |
| 部署一直停在"正在部署" | 上次上传中断了。重新部署一次即可覆盖 |
| 内容更新了但页面没变 | 先加 ?fresh=1 排除边缘缓存;如果是浏览器自己的副本,等 max-age 过期或强制刷新 |
| 项目名提示"已被占用" | 名字被别的项目用了。删掉旧项目或换个名字 |
├─ wrangler.jsonc 主配置(不含 routes,原因见本地开发一节的警告)
├─ wrangler.deploy.jsonc 部署时自动生成,含两条路由,不要手改
├─ schema.sql D1 建表语句
├─ .dev.vars.example 本地密钥模板
├─ src/
│ ├─ index.js 入口:按 Host 分流入口页 / 后台 / 项目 / 保留子域名
│ ├─ landing.js 对外入口页(服务端渲染 + 内联 CSS + 搜索过滤)
│ ├─ api.js 登录、项目增删、分片上传、finalize、残留清理
│ ├─ serve.js R2 读取、Content-Type、ETag、边缘缓存
│ ├─ auth.js HMAC 签名会话 Cookie、常量时间口令比较
│ ├─ mime.js 扩展名 → Content-Type、缓存时长
│ └─ util.js 主机名解析、项目名校验、路径规范化
├─ public/ 管理后台(由 Worker 自己托管)
│ ├─ index.html
│ ├─ app.js 登录、上传编排、进度与重试、列表管理
│ ├─ style.css
│ ├─ unzip-worker.js Web Worker:两遍扫描(先读中央目录做规划,再流式解压分片)
│ └─ vendor/fflate.js 由 npm run vendor 从 node_modules 拷入,不进版本库
└─ scripts/
├─ vendor.mjs 拷贝 fflate
├─ preflight.mjs 部署前配置体检
├─ build-config.mjs 生成带路由的部署配置
└─ smoke-test.mjs 60 项端到端断言
| 层 | 选型 | 说明 |
|---|---|---|
| 运行时 | Cloudflare Workers | 免费版,100k 请求/天 |
| 存储 | Cloudflare R2 | S3 兼容,出口流量免费 |
| 元数据 | Cloudflare D1 | 服务端 SQLite |
| 解压 | fflate | 纯 JS,浏览器端流式解压 |
| 前端 | 原生 ES Modules | 无框架、无构建步骤 |
| 测试 | Node 原生脚本 | npm run smoke,60 项端到端断言 |
MIT © 2026 cokar