feat(assets): inject ASSET_URLS into index.html for direct CDN loading - #60
Wudarensheng wants to merge 10 commits into
Conversation
Replace the broken 302-redirect ASSET_URLS implementation with HTML injection of window.OPENLIST_CONFIG.cdn, as discussed in issue OpenListTeam#26. The browser then loads JS/CSS/images directly from the CDN, bypassing the Worker for the dozens of per-page asset requests (1-2 orders of magnitude fewer Worker invocations). - Remove the non-functional /:folder/:filepath* 302 route (filepath param was always undefined, multi-segment paths never matched, and db.get() targeted a method absent on map-mode DB) - Add resolveCdnUrl() + injectCdnIntoHtml() mirroring Go's server/static/static.go cdn injection; resolves best-effort from the version setting, falling back to "latest", and skips DB reads entirely when no $version placeholder is present - Wire injection into all three HTML-serving paths in index.ts (ASSETS entry, ASSETS SPA fallback, spaFallbackHtml) - Add unit + integration tests (14 passing) - Document ASSET_URLS in .env.example / .dev.vars.example / wrangler.jsonc
|
感谢您的贡献,可以看看和#52 的差异 |
|
这里有 3 个需要重新确认的问题:
现在的逻辑是在 Worker 里: const res = await env.ASSETS.fetch(...)
const html = await res.text()
html = await injectCdnIntoHtml(html, env)
return new Response(html, ...)但如果 Cloudflare Static Assets 已经直接命中 也就是说,“Worker 内部直接读取并输出 HTML”并不能解决这个问题,因为问题发生在 Worker 执行之前。 如果继续采用服务端修改
这两个配置的语义本身就是修改最终 HTML 的 前端注入会带来几个明显区别:
所以我建议不要因为之前 Cloudflare 的 asset-first 路由导致“后端注入不生效”,就把注入职责移动到前端。真正需要修的是 HTML 请求如何可靠进入后端处理链路。 而且 当前 PR #60 现在实际上又重新采用了“后端读取 index HTML → 修改 → 返回”的方式来注入 |
这个是需要斟酌的点: 我更倾向于1,也就是 |
|
但我还是不太接受前端 runtime 注入,尤其是 |
|
至于CDN这块 其实无所谓 前端拿到以后为后端补齐域名/后端提前返回 是差不多的 |
|
这里有个考虑的点,就是大部分用户其实是不会自定义head和body的 对,CDN其实无所谓,直接返回指向和先载入前端区别并不大 |
|
客户端js注入前端呢? |
Configuring ASSET_URLS produced a flood of 404s (302 -> not found on
registry CDNs) and a blank page, because the served HTML was the local
build while the CDN served a different frontend build:
- $version always resolved to "latest" — the DB version setting never
carries a "Frontend:" segment, so the resolution path was dead code
- even a correct version number does not match: hashed asset names are
build-specific (local main build index-CKHDcXcr.js vs npm 4.2.6
index-CelfHslL.js), so every /assets/* request 404s on the CDN
Fix, mirroring Go's server/static/static.go initIndex():
- getIndexHtmlWithCdn(): fetch ${cdn}/index.html (4s timeout, module
cache with TTL, http(s) only, HTML sanity check), inject cdn into it,
and serve that. HTML and hashed assets now come from the same build.
- Graceful degradation: if the CDN is unreachable or returns non-HTML,
fall back to the local HTML WITHOUT injecting cdn, so assets load from
the origin and the site keeps working instead of white-screening.
- fetch-frontend.mjs stamps <meta name="frontend-version"> into
dist/index.html (guarded to the official frontend package name), so
$version resolves to the version actually deployed.
- $version priority: build stamp -> DB "Frontend:" -> latest.
Tests: 17 unit + 6 integration (all passing); tsc clean; the 3 failing
server tests (F-11 x2, CAS) are pre-existing and unrelated.
Cloudflare 的静态资源路由默认「资源优先」:/ 与 /index.html 由静态层直接返回, Worker 脚本根本不执行,导致 ASSET_URLS 的 CDN 注入永远不生效 —— 浏览器拿到 仍是 cdn: undefined 的源站 HTML,所有 JS/CSS/图片继续从 Worker 域名加载。 修复: - wrangler.jsonc 增加 assets.run_worker_first: ["/", "/index.html"],只放行 HTML 入口。带 hash 的 JS/CSS/图片仍由静态层直出,零 Worker 调用;仅页面导航 多付一次 Worker 调用。 - 未配置 ASSET_URLS 时零开销直通:不读取 body,直接流式透传静态层响应, 非 CDN 用户不为本次改动额外付出 HTML 缓冲与解析成本。 - 改写 HTML(注入 cdn)时清掉 content-encoding / content-length:body 已被读成 字符串,保留编码头会让浏览器按「已编码」解析明文而报错。 测试:assets.test.ts + index_cdn.test.ts 25/25 通过(新增 2 例:零开销直通断言 body 流被原样透传、注入路径清编码头);tsc --noEmit 通过;wrangler deploy --dry-run 配置校验通过。全量 server 套件 62/65,3 个失败为改动前既有。
…DN requirements
实测结论(2026-09-18):
registry.npmmirror.com/<pkg>/<ver>/files/
dist/index.html -> 451 {"error":"blocked"}(该端点只放行 js/css,拦截 .html)
dist/assets/*.js -> 200,但【不返回 Access-Control-Allow-Origin】
cdn.npmmirror.com/packages/<pkg>/<ver>/files/
dist/index.html -> 200
dist/assets/*.js -> 200,同样【不返回 Access-Control-Allow-Origin】,且不支持 latest
前端产物以 crossorigin 加载 module script / modulepreload / stylesheet / 字体,
缺少 CORS 头会被浏览器整批拦下;因此 npmmirror 的两个端点都无法作为 ASSET_URLS。
jsdelivr 与 unpkg 均返回 Access-Control-Allow-Origin: *,实测可用。
把文档里原先推荐的 npmmirror 示例换成 unpkg,并写明 ASSET_URLS 必须同时满足
「能返回 index.html」与「带 Access-Control-Allow-Origin」两个硬性条件。
代码逻辑无需改动:CDN 返回非 HTML(如 451 JSON)时已有降级回退。
PR #52 / #60 / #671 评审与「自定义 header·body」修复决策
1. 结论摘要(TL;DR)
2. 基本事实
3. 背景时间线
4. 三个 PR 各自做了什么4.1 #60(
|
| 占位符 | 设置键 | 备注 |
|---|---|---|
<!-- customize head --> |
customize_head |
原样注入 |
<!-- customize body --> |
customize_body |
原样注入 |
https://res.oplist.org/logo/logo.svg |
favicon |
|
https://res.oplist.org/logo/logo.png |
logo |
只取第一行 |
<title>Loading...</title> |
site_title |
|
main_color: undefined |
main_color |
包成 main_color: '<value>' |
4.3 #671(前端运行时注入)
src/utils/customize.ts(新增 43 行):<template>解析 HTML → 对<script>节点手工重建(复制属性 + textContent)再 append,保证自定义 JS 真正执行;非 script 节点cloneNode(true)后 append;分别注入document.head/document.body。src/app/App.tsx:在/public/settings返回后调用applyCustomize(),并用isTsWorker()门控,避免 Go 后端重复注入。- 第 3 个 commit(
05b634c)夹带了backup-restore.tsx的解密修复(加密备份恢复后settings仍是密文)。
5. 实现方式对照
| 维度 | #52 | #60 |
|---|---|---|
| 改写的 HTML 位置 | 同一处三块:env.ASSETS 的 / + /index.html、SPA fallback、内联 spaFallbackHtml |
同一处三块(完全相同) |
| 转换方式 | 6 组 split().join() 占位符替换 |
拉 CDN HTML 后 replace(/cdn:\s*undefined/, ...) |
| 缓存 | WeakMap<env, string>,无 TTL,靠 saveDb() 通知失效 |
Map<cdnUrl, {html, ts}>,TTL 5 分钟 |
| 降级 | 读 DB 失败 → 原始 HTML(不 500) | CDN 不可达 / 非 HTML → 本地 HTML 且不注入 cdn |
| 平台配置 | ❌ 未改 wrangler.jsonc(CF 首页不生效) |
✅ run_worker_first: ["/", "/index.html"] |
| 响应头 | 复用原 headers,未删 content-encoding / content-length |
/ 分支明确删除这两个头 |
| 附带修复 | — | 修掉 db.get("SELECT * FROM x_settings ...")(map 格式存储上不存在的方法);$version 三级解析(构建期 meta 戳 → DB「Frontend: vX」→ latest) |
| 测试 | 10 个(8 单测 + 2 集成,直接打真实 Hono app) | 9 单测 + 5 集成 |
| 文档 | 无 | .env.example / .dev.vars.example / wrangler.jsonc 注释 |
冲突面:两者改 src/backend/index.ts 里同一个 app.all("*") 的三块分支,文本必然冲突,语义必须串成一条管线。
6. 关键关系:不是二选一
6.1 #60 给 #52 补上了唯一缺失的拼图
origin/main 的 wrangler.jsonc 只有:
"assets": {
"directory": "./dist",
"binding": "ASSETS"
}全仓库 grep run_worker_first = 0 命中。而 CF 静态资源默认「资源优先」:/ 与 /index.html 命中 dist/index.html 时由静态层直出、Worker 不执行 → #52 的注入在首页是死代码。#60 的 run_worker_first 正是解药,且它的 .env.example 注释里已经写明这个前提。
6.2 组合必须是「先 CDN 后设置」
若两 PR 都合,必须在 CDN 拉回来的那份 HTML 上做设置注入,否则官方 HTML 会覆盖已注入的设置:
// 目标形态(示意)
let html = template
if (isCdnConfigured(env)) html = await getIndexHtmlWithCdn(env, html) // #60:定稿 HTML
html = await buildIndexHtml(html, env) // #52:在定稿 HTML 上注入设置
return htmlResponse(html, status, baseHeaders) // 统一头:删 content-encoding/length + no-cache可行性已确认:官方前端 index.html 中确实保留了 <!-- customize head --> / <!-- customize body --> 占位符,因此从 CDN 拉回的 HTML 同样可被 #52 注入。
6.3 生效矩阵
| 部署形态 | #52 单独 | #52 + #60 | #671(前端) |
|---|---|---|---|
CF Workers / 首页 |
❌ 静态层直出 | ✅ | ✅ |
| CF Workers 深链 fallback | ✅ | ✅ | ✅ |
| EdgeOne(Node 云函数) | ❌ | ❌ 仍不生效 | ✅ |
| ESA | ✅(大概率) | ✅ | ✅ |
纯静态 / CDN + ASSET_URLS |
❌ | ❌ | ✅ |
EdgeOne 是唯一"#52 + #60 都合了也修不好"的平台:导航请求在边缘中间件就被改写到静态资源:
if (
!isBackend &&
(request.method === "GET" || request.method === "HEAD") &&
accept.includes("text/html")
) {
return rewrite("/index.html")
} "rewrites": [
{
"source": "/*",
"destination": "/index.html"
}
],→ 页面导航由静态 CDN 直出 dist/index.html,Node 云函数(api/_makers.ts → spaFallbackHtml 分支)拿不到页面请求。
要修只有两条路:(a) 让导航请求进 Node 云函数(放行 / 与前端路由,代价是每次页面加载一次函数调用);(b) 交给前端 #671。
7. 待修问题清单
7.1 硬阻断
| 编号 | 问题 | 证据 |
|---|---|---|
| B-1 | #60 未提交重建后的 cloud-functions/[[default]].js → EdgeOne Artifact Guard 失败(exit 1),且 EdgeOne 线上仍是旧代码 |
GitHub checks 实测:Fail on out-of-sync artifact (pull request);git diff --name-only origin/main...pr-60 的 8 个文件中不含该产物;守卫规则见 .github/workflows/edgeone-artifact-guard.yml:102-107 |
| B-2 | #52 落后 origin/main 7 个 commit,产物与当前源码不匹配 |
git merge-base origin/main fix/customize-html-injection → 5cac7eb;git rev-list --count fix/customize-html-injection..origin/main → 7(#54/#56/#50/#57/#59/#53/#58)。其中 db.ts、store/json.ts、public.ts、raw.ts、fs.ts 均被改动,产物必然不一致 → 需 rebase + 重建 |
| B-3 | 两个 PR 都提交了 1.5MB 生成产物,先合的一方会让另一方产物失效 | 产物冲突不要手工解,rebase 后执行 node scripts/build-edge.mjs 重新生成 |
7.2 #60 的具体问题(4 点)
- SPA fallback 分支无条件缓冲 body:
/分支有isCdnConfigured()零开销直通,fallback 分支却是let html = await rootRes.text()无条件执行 —— 未配置ASSET_URLS时也白付一次缓冲 + 重建 Response,丢掉了原来的流式透传。 - 非 2xx 被包装成 200:原实现
return env.ASSETS.fetch(rootReq)原样透传状态码;现在固定new Response(html, { status: 200 }),若子请求返回 307/404 会变成 200 + 错误正文。([废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52 在这一点上反而更稳:它先判断 2xx 才改写。) injectCdnIntoHtml单引号未转义:cdn: '${cdn}',ASSET_URLS含'会破坏内联脚本。对比 [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52 对main_color特意做了引号/反斜杠转义,同一类风险处理不一致。- 删除 302 路由属 breaking change:
assetsRouter.get("/:folder/:filepath*")整体移除。原实现确实有问题(db.get("SELECT ...")在 map 格式存储上不存在),但 PR 标了 breaking 却无迁移说明,建议补 release note / README 说明。
7.3 #52 的具体问题(3 点)
wrangler.jsonc未加run_worker_first→ CF 首页不注入(由 feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 补齐)。- 改写 body 后未删
content-encoding/content-length:而 feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 在同一个文件的同一分支里明确删掉了,并注释"否则浏览器按「已编码」解析明文"。同一代码库已有正确做法,说明这不是理论风险。 - 缓存设计不一致:
WeakMap<env, string>且无 TTL → 跨 isolate 失效不了(其他 POP 的 isolate 会一直返回旧 HTML,直到被回收)。DB 缓存有 1s TTL、feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 的 CDN HTML 缓存有 5min TTL,唯独 HTML 注入结果没有。建议改用环境无关的版本戳(DBupdated_at)或加短 TTL。
7.4 #671 的具体问题(3 点)
applyCustomize()无幂等:App 只要重新挂载一次(路由重建、HMR、未来某处再调一次)就会重复注入。建议加模块级 flag 或document.head.dataset.openlistCustomize标记。- 夹带无关改动:第 3 个 commit(
backup-restore.tsx解密修复)本身是对的(加密备份恢复后customize_head/body仍是密文),但应拆为独立 PR。 - 无测试:PR 正文测试项为空,
injectFragment至少需覆盖「<script>被重建并执行」与「重复调用不重复注入」。
8. 需求对照
| 编号 | 需求 | 提出方 | #52(+#60) | #671 |
|---|---|---|---|---|
| R1 | TS 后端下 customize_head/body 生效 |
共同 | ✅ | ✅ |
| R2 | 不与 Go 后端重复注入 | 共同 | ✅ | ✅(isTsWorker() 门控) |
| R3 | 纯静态部署也生效 | PIKACHUIM | ❌ | ✅ |
| R4 | 不让所有页面请求走计算层 | PIKACHUIM | ❌(需 run_worker_first) |
✅ |
| R5 | <head> 语义 / 无 JS 可用 / SEO 可见 |
JYXJJJ | ✅ | ❌ |
| R6 | 改完设置尽快生效、无需冷启动 | 共同 | ✅ | |
| R7 | 与其它 PR 可共存、无双实现分叉 | 共同 | ✅ |
功能性设置覆盖(走 #671 后)
| 设置项 | 是否生效 | 说明 |
|---|---|---|
customize_head / customize_body |
✅ | 核心目标达成 |
site_title |
前端 useTitle 会改 document.title;但初始 HTML 的 <title> 仍为 Loading...,爬虫/社交卡片读到旧值 |
|
main_color |
✅ | getMainColor() 兜底读 getSetting("main_color"),只差首屏一帧默认色 |
logo |
✅ | 前端 Login/Header/Init 自行读 settings |
favicon |
❌ | 前端 .ts/.tsx 中 favicon 零引用;index.html 硬编码 https://res.oplist.org/logo/logo.svg |
apple-touch-icon |
❌ | 同上,硬编码 logo.png |
补充:要判断 UI 是否误判,注意 /api/public/settings 一直正确返回这些字段(customize_head/body 的默认值在 public.ts 的 settingsObj 中,属于公开白名单),所以接口层看起来完全正常。
9. 建议路线
路线 A(推荐,若接受 R5 的取舍):前端注入
- 合:#671(+ 幂等 + 测试 + 拆分 backup commit,可选顺带补「运行时改
favicon的 href」以对齐观感)。 - 不合:[废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52(与 #671 互斥;二者同时存在会造成双注入)。
- feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60:与 customize 无关,可独立合(有 CDN 价值),但必须先修 7.2 的 4 点并重建产物。
路线 B:服务端注入(需要 SEO / 无 JS / <head> 语义硬需求)
- 顺序:feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 → [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52(不能反)。
- 额外工作:
- feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 合并前:修 7.2 四点 + 重建
cloud-functions/[[default]].js; - [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52 rebase 到 feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 之上,抽成统一管线(见 9.3),补 7.3 三点;
- CF:确认
run_worker_first生效(curl -I https://<域名>/应带cache-control: no-cache, must-revalidate); - EdgeOne:放行
/与前端路由进 Node 云函数(改middleware.js/edgeone.json),否则仍不生效; - 与 #671 的互斥:必须给服务端输出打标记(如
<meta name="openlist-customize" content="1">),前端检测到即跳过,或直接关闭 #671。
- feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 合并前:修 7.2 四点 + 重建
路线 C(混合,成本最高)
服务端做主路径(#60 + #52 + 平台改道),前端 #671 作为兜底(带标记互斥)。只有同时需要"EdgeOne/纯静态可用"与"SEO/无 JS 可用"才值得。
9.3 统一 HTML 管线(路线 B 的落地形态)
resolveTemplate(ASSETS /内联壳 / CDN index.html)
→ #60 getIndexHtmlWithCdn // 未配置 ASSET_URLS 时零开销直通
→ #52 buildIndexHtml // 注入 customize / site_title / favicon / main_color
→ htmlResponse // 统一 Content-Type + no-cache,删除 content-encoding / content-length
缓存:只缓存最后一环,带 TTL + DB 写入失效(替代 WeakMap<env> 无 TTL 方案)
10. 合并顺序与 checklist
若走路线 A
- #671:加幂等标记;补 2 个单测;把 backup 解密 commit 拆出或单独说明
- #671:决定是否顺带补 favicon 的运行时替换(否则明确记录"favicon 设置不支持")
- [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52:标注为「被 #671 取代」并关闭(保留结论与取舍说明,避免后人再走一遍
fa3437c的历史) - feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60:修 7.2 四点(fallback 加
isCdnConfigured守卫 / 保留原状态码 / cdn 转义 / 补 breaking 说明) - feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60:
node scripts/build-edge.mjs重建并提交cloud-functions/[[default]].js,确认 Artifact Guard 转绿
若走路线 B
- feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60:同上的四点修复 + 产物重建,先合
- [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52:rebase 到 feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 之上;抽出统一管线;补
content-encoding/length清理;缓存改版本戳或加 TTL - EdgeOne:
middleware.js/edgeone.json放行/与前端路由进云函数 - #671:打标记互斥或关闭
- 实测:CF
/、EdgeOne/、深链刷新三处都验证 customize 生效 -
npm run test:server+npx tsc --noEmit+ 平台手测
11. 待确认
- Telegram 投票 / 团队决议结果:customize 走「服务端 transform」还是「前端运行时」?(决定路线 A / B)
- EdgeOne 是否接受"页面导航进 Node 云函数"?这是服务端路线在 EdgeOne 上的唯一解法(代价是每次导航一次函数调用)。
- feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 删除
/:folder/:filepath*302 路由是否可作为 breaking change 合入(是否需要过渡期/文档说明)。 favicon设置是否必须修(决定是否要给 #671 补运行时替换,或保留 [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52 的这一部分能力)。
附录 A:核查方法与证据
:: 1) 拉取 PR #60 分支并对比
git fetch origin pull/60/head:pr-60 --no-tags
git --no-pager diff --stat origin/main...pr-60
git --no-pager diff origin/main...pr-60 -- src/backend/index.ts wrangler.jsonc scripts/fetch-frontend.mjs
git --no-pager show pr-60:src/backend/server/assets.ts
:: 2) 判断 #52 是否落后 main
git --no-pager merge-base origin/main fix/customize-html-injection :: 5cac7eb
git --no-pager rev-list --count fix/customize-html-injection..origin/main :: 7
git --no-pager diff --name-only 5cac7eb origin/main
:: 3) 确认平台路由前提
findstr /S /I "run_worker_first" * :: 0 命中
:: 生效自检(合并后)
curl -I https://<你的CF域名>/ :: 期望 cache-control: no-cache, must-revalidate已确认的关键事实:
| 事实 | 结论 |
|---|---|
origin/main 无 run_worker_first |
#52 在 CF 首页不生效,需 #60 补齐 |
CF 资源优先 → /、/index.html 由静态层直出 |
之前那次 no-cache 修复在 / 上同样可能从未生效 |
middleware.js + edgeone.json 把导航 rewrite 到静态 index.html |
#52/#60 在 EdgeOne 上均不生效 |
public.ts 的 settingsObj 含 customize_head/body |
/api/public/settings 会返回它们,#671 的 getSetting() 有值 |
settings.ts 的 setSettings() 同步调用 setBackendKind() |
#671 中 setSettings(data) 之后紧跟的 isTsWorker() 时序正确 |
前端 .ts/.tsx 中 favicon 零引用 |
走 #671 后 favicon 设置仍失效 |
#60 的 8 个变更文件中不含 cloud-functions/[[default]].js |
Artifact Guard 失败(已实测) |
fix/customize-html-injection 距今落后 7 个 commit |
需 rebase + 重建产物 |
…mirror support)
原实现总是从 `${ASSET_URLS}/index.html` 取 HTML,于是撞上 npmmirror 的
反滥用规则:registry.npmmirror.com/<pkg>/<ver>/files/ 对所有包的 .html 一律
返回 451 {"error":"blocked"}(js/css/json 正常放行),导致 ASSET_URLS 指向
npmmirror 时注入永远失败。Go 版不受影响是因为 Release 版用内置 index.html,
只有资产走 CDN —— 官方文档原文:"Some NPM CDNs (like npmmirror) may prohibit
access to HTML files, but Release versions don't depend on CDN's index.html,
so they're unaffected"。
改为两条路径,自动选择:
A. 本地 index.html + CDN 资产(首选,等价 Go Release 行为)
先 HEAD 探测 CDN 上是否存在本地 HTML 引用的哈希入口资产;存在则直接下发
本地 HTML 并注入 cdn。不要求 CDN 能返回 HTML,npmmirror 因此可用。
哈希文件名内容寻址,入口存在即代表整套哈希一致,不会引到 404 上。
B. CDN 的 index.html(本地构建与 CDN 版本不一致时兜底)
保持原有行为,HTML 与哈希资产天然同源一致。
A、B 都不可用 → 不注入,资源回退源站(不会白屏)。
顺带修正上一版文档里的错误结论:npmmirror 并非缺少 CORS —— 它在请求带 Origin
时回显 Access-Control-Allow-Origin(此前用 curl 不带 Origin 测试,误判为不发
CORS 头)。真正不可用的只有 .html 这一项,而新路径已不再依赖它。
测试:assets.test.ts 22 例 + index_cdn.test.ts 10 例全部通过(新增
extractEntryAsset 单测、两条路径的调用顺序断言、以及「探测失败 + CDN 拦截
.html 时降级不注入」);tsc --noEmit 通过;wrangler dry-run 配置校验通过。
|
暂时不再合并#52 等本PR修改完善后合并本PR和前端#671 |
对齐 Go 版 server/static/static.go:配置 cdn 后 /assets/、/images/、/streamer/、 /static/ 下的请求交给 CDN。Cloudflare 资源优先路由下资源存在时由静态层直出、 Worker 不执行,所以只在源站确实缺失时才触发 —— 此时原先会落到 SPA 兜底,把 index.html 当作 .js/.css 返回,浏览器按 text/html 解析后报错。 - cdnAssetRedirect():仅匹配「目录/具体文件」,目录本身与前端路由不重定向。 - 复用 HTML 那次解析出的 CDN 地址(按 ASSET_URLS 原文缓存),保证 替换结果与 HTML 指向同一版本;冷启动直接命中资源时退回按 env 解析。 - 同时接在 ASSETS 未命中的分支与 EdgeOne/ESA 无 ASSETS 的兜底分支上。 测试:37/37 通过(新增 4 例:三个静态目录的重定向、前端路由与目录本身不重定向、 解析、以及集成层面的 302 断言);tsc 与 wrangler dry-run 通过。
- SPA 兜底分支不再无条件把 body 读成字符串并固定 status: 200:只有「已配置 ASSET_URLS + 2xx + GET/HEAD」才改写 HTML,其余情况按「状态码 + headers + body 流」原样透传,避免把 307/404 抹成 200 空壳,并保住未配置 CDN 时的 流式直通。 - spaFallbackHtml 分支同样只在配置了 ASSET_URLS 时才走 CDN 注入。 - injectCdnIntoHtml 对注入值做 JS 字面量转义(单引号、反斜杠、换行),避免 URL 中含引号时提前闭合内联脚本、令 window.OPENLIST_CONFIG 语法报错、整站 白屏(Go 版同样未处理,此处不与其缺陷对齐)。 - .env.example / .dev.vars.example 补记移除 /:folder/:filepath* 302 路由这一 行为变更与迁移说明。 - 新增 4 个测试:引号与反斜杠转义、SPA 兜底流式透传、非 2xx 保留状态码、 已配置 CDN 时 307 仍透传。
前端产物是内容哈希文件名,而 CDN(jsdelivr / unpkg / npmmirror)提供的正是 npm 包里那一份 dist。原先默认「克隆前端 main 现构建」,产物哈希与 CDN 不一致, ASSET_URLS 的路径 A(下发本地 index.html + CDN 资产)HEAD 探测必然失败,只能 退化到拉 CDN 的 index.html;而 npmmirror 等镜像禁止访问 .html(451),于是 CDN 完全用不了。 - scripts/fetch-frontend.mjs 默认改为下载 npm 上【已发布】的 dist(版本取 registry 的 latest,可用 FRONTEND_VERSION 固定);只解出 package/dist 与 package/package.json,后者供 stampFrontendVersion 读出真实发布版本号,使 ASSET_URLS 的 $version 正好解析到这份 dist 对应的版本。 - 原「克隆 main 现构建」保留在 FRONTEND_BUILD_FROM_SOURCE=1 之后,供开发联调。 - 下载失败直接报错终止,不静默回退到现构建:那条路的哈希与 CDN 不一致,会让 路径 A 悄悄失效(npmmirror 直接不可用)。 - 修 Windows 上 tar 把 "C:\..." 盘符冒号当远程主机的问题:以 tmp 为 cwd、 用相对路径解包。 - .env.example / .dev.vars.example 补记路径 A 对「同一份构建」的要求。 实测(wrangler dev + 真实 CDN):npmmirror 现在能注入并命中路径 A,$version 解析为 4.2.6,注入的入口资产在 CDN 上返回 200 + CORS。
|
@PIKACHUIM 测试都过了 |
Summary / 摘要
本 PR 按 OpenList-Worker Issue #26 优化
ASSET_URLS功能。将原先的静态资源 Worker 302 重定向改为在
index.html中注入window.OPENLIST_CONFIG.cdn。浏览器后续直接向 CDN 请求 JS、CSS、图片等静态资源,不再让每个资源请求都经过 Worker。
未配置
ASSET_URLS时保持现有行为。支持
$version占位符:从数据库version设置中提取前端版本;无法解析时回退为latest。未使用
$version时不读取数据库,避免额外存储请求。覆盖 Cloudflare
ASSETS、Cloudflare SPA fallback、EdgeOne/ESAspaFallbackHtml三种 HTML 返回路径。保留 logo/favicon 兼容性重定向。
删除原有不可用的
/:folder/:filepath*302 路由:该实现无法正确取得filepath,且不适配 map 数据库模式。增加单元测试与集成测试,并补充环境变量文档。
This PR has breaking changes.
/ 此 PR 包含破坏性变更。
This PR changes public API, config, storage format, or migration behavior.
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
说明:新增公开配置项
ASSET_URLS;不修改存储格式或迁移行为。This PR requires corresponding changes in related repositories.
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
Related Issues / 关联 Issue
Relates to #26
Testing / 测试
已在 Windows 11、Node.js v24.16.0 环境执行:
go test ./...(不适用:本仓库为 TypeScript/Hono 项目,无 Go 测试套件)npx tsx --test src/backend/server/assets.test.ts9/9 通过。
npx tsx --test src/backend/server/index_cdn.test.ts5/5 通过,覆盖 ASSETS 入口、ASSETS SPA fallback、无 ASSETS 的
spaFallbackHtml以及$version回退。npx tsc -p tsconfig.json --noEmit类型检查通过。
npx tsx --test "src/backend/server/*.test.ts"54 个测试中 51 个通过,3 个失败为本次改动前已存在的 F-11/CAS 测试失败;新增 ASSET_URLS 测试均通过。
未在实际 CDN 部署环境中进行浏览器手动验证;已使用模拟 ASSETS 绑定完成请求级集成验证。
Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。说明:本次已完成 TypeScript 类型检查,但未单独运行 Prettier。
/ 我已在适用情况下请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Refactoring / 重构
Documentation / 文档
Tests / 测试
Translation / 翻译
Review assistance / 审查辅助
I have reviewed and validated all AI-assisted content included in this PR.
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
I have ensured that all AI-assisted commits include
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交都包含
Co-Authored-By归属信息。说明:当前提交未包含
Co-Authored-By归属信息,故不勾选。I can reproduce all AI-assisted content included in this PR without any AI tools.
/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。