From ca2fb7163ddc1729e9a983e5b2069e54d45362a6 Mon Sep 17 00:00:00 2001 From: zqlit Date: Tue, 6 Oct 2026 13:28:56 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=BC=80=E5=90=AF=20Workers=20Cache=20?= =?UTF-8?q?=E7=BC=93=E5=AD=98=20favicon=EF=BC=8C=E5=B9=B6=E7=BB=99?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E6=8E=A5=E5=8F=A3=E5=85=9C=E5=BA=95=20no-sto?= =?UTF-8?q?re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:favicon 此前只有 Cache-Control: max-age(仅浏览器缓存), CF 边缘不缓存 Worker 响应 → 每次请求都进 Worker → 第一句就是 KV.get。 友圈页按友链数放大,KV 读量被显著放大。 ★ 关键纠错:zone 级 Cache Rules 对 Worker 响应**完全无效**。 原因是 Worker 位于 zone 缓存之前("Workers sits in front of cache"), 且 Worker 直接生成响应时不发 fetch 子请求,cache status 只能是 none/unknown。 查证 CF 官方文档后确认,正确方案是 [cache] enabled = true(Workers Cache, 2026-07 上线,需 wrangler >= 4.69)。 改动: - wrangler.toml: 新增 [cache] enabled = true;compatibility_date 提到 2026-07-24 - package.json: wrangler ^3.99 → ^4.147,workers-types ^4 → ^5(peer 要求) - index.ts: 抽出 dispatchRequest;新增 ensureCachePolicy 兜底 ★ 这一步是必须的:Workers Cache 遵循 RFC 9111 含**启发式缓存**, 没带 Cache-Control 的响应也可能被缓存。实测 /api/v2/comments 开启后 立刻被缓存(HIT, age:29)→ 用户发的新评论看不见。 现在白名单式兜底:已显式声明可缓存的(favicon 的 s-maxage)保留, 其余一律补 no-store。 验证(中转机实测,已清空 zone 的无效 cache rule): - /api/favicon → MISS → HIT,age 递增(Worker 不再执行,KV 读归零) - /api/v2/comments → BYPASS ×3,cache-control: no-store - /api/v2/conf → BYPASS,no-store - /api/v2/healthz → BYPASS,no-store --- blog-admin/package.json | 4 +- blog-admin/src/index.ts | 94 ++++++++++++++++++++++++++++++---------- blog-admin/wrangler.toml | 19 +++++++- 3 files changed, 91 insertions(+), 26 deletions(-) diff --git a/blog-admin/package.json b/blog-admin/package.json index bdb4ceed..e3808ca7 100644 --- a/blog-admin/package.json +++ b/blog-admin/package.json @@ -14,8 +14,8 @@ "tail": "wrangler tail" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20250109.0", + "@cloudflare/workers-types": "^5.20261006.1", "typescript": "^5.7.0", - "wrangler": "^3.99.0" + "wrangler": "^4.147.0" } } diff --git a/blog-admin/src/index.ts b/blog-admin/src/index.ts index 364cb675..6e1d57c7 100644 --- a/blog-admin/src/index.ts +++ b/blog-admin/src/index.ts @@ -122,34 +122,82 @@ router.get('/transfer/export', A.transferExport); router.post('/transfer/import', A.transferImport); router.post('/transfer/upload', A.transferUpload); +// ---------------------------------------------------------------- 请求分发 + +async function dispatchRequest( + req: Request, + env: Env, + ctx: ExecutionContext, + url: URL, +): Promise { + // R2 里的图片:/file/** 或 /api/v2/file/** + if (req.method === 'GET' && /^(\/api\/v2)?\/file\//.test(url.pathname)) { + return A.serveFile({ + env, + req, + url, + params: {}, + user: null, + }); + } + + // RSS 订阅模块(rss-robot 合并进来):/api/*(Artalk 用 /api/v2/* 不会撞) + // + /feed 订阅源管理页 + 微信验证文件 + const p = url.pathname; + if ( + (p.startsWith('/api/') && !p.startsWith('/api/v2')) || + p === '/feed' || + p.startsWith('/MP_verify_') + ) { + return RssApi.fetch(req, env, ctx); + } + + return router.dispatch(req, env, ctx); +} + +// ---------------------------------------------------------------- 缓存策略兜底 +// +// ★ 为什么需要这个:Worker 开启了 Workers Cache(wrangler.toml 的 [cache] +// enabled = true)之后,Cloudflare 会在**调用 Worker 之前**先查缓存。 +// 而 Workers Cache 遵循 RFC 9111 —— 包括**启发式缓存**:一个完全不带 +// Cache-Control 的响应也可能被判为可缓存! +// +// 实测教训:`/api/v2/comments` 本来就没设 Cache-Control,开启 Workers Cache +// 后立刻被缓存(cf-cache-status: HIT, age: 29)→ 用户发的新评论看不见。 +// +// 规则(白名单式,只放行明确声明可缓存的): +// - 上游已带任意 Cache-Control / CDN-Cache-Control → 原样保留(如 favicon 的 +// s-maxage=86400,它本来就该缓存) +// - 其余一律补 `Cache-Control: no-store`(评论、登录、管理接口等禁止缓存) +// +// ★ 必须是白名单思路:漏掉一个接口 = 数据陈旧 bug;而多补 no-store 只损失一点性能。 +function ensureCachePolicy(res: Response, req: Request): Response { + // 只处理 GET/HEAD(Workers Cache 本就只缓存这两种;其余原样返回) + if (req.method !== 'GET' && req.method !== 'HEAD') return res; + + const hasExplicitPolicy = + res.headers.has('Cache-Control') || + res.headers.has('CDN-Cache-Control') || + res.headers.has('Cloudflare-CDN-Cache-Control'); + if (hasExplicitPolicy) return res; + + // 其余(含 3xx / 5xx)一律禁止缓存:宁可少缓存,不可把数据或错误固化 + const headers = new Headers(res.headers); + headers.set('Cache-Control', 'no-store'); + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers, + }); +} + export default { async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(req.url); try { - // R2 里的图片:/file/** 或 /api/v2/file/** - if (req.method === 'GET' && /^(\/api\/v2)?\/file\//.test(url.pathname)) { - return A.serveFile({ - env, - req, - url, - params: {}, - user: null, - }); - } - - // RSS 订阅模块(rss-robot 合并进来):/api/*(Artalk 用 /api/v2/* 不会撞) - // + /feed 订阅源管理页 + 微信验证文件 - const p = url.pathname; - if ( - (p.startsWith('/api/') && !p.startsWith('/api/v2')) || - p === '/feed' || - p.startsWith('/MP_verify_') - ) { - return RssApi.fetch(req, env, ctx); - } - - return router.dispatch(req, env, ctx); + const res = await dispatchRequest(req, env, ctx, url); + return ensureCachePolicy(res, req); } catch (e) { // 兜底:任何漏网的异常也必须带 CORS 头,否则浏览器把它当"网络故障", // 读者只能看到 "TypeError: Failed to fetch",根本不知道发生了什么。 diff --git a/blog-admin/wrangler.toml b/blog-admin/wrangler.toml index 87255a8f..35f001da 100644 --- a/blog-admin/wrangler.toml +++ b/blog-admin/wrangler.toml @@ -1,6 +1,23 @@ name = "artalk-cf" main = "src/index.ts" -compatibility_date = "2025-06-01" +compatibility_date = "2026-07-24" + +# --------------------------------------------------------------------------- +# ★ Workers Cache —— 缓存 Worker 自己生成的响应(无需回源 fetch) +# +# 为什么必须开:Worker 位于 zone 缓存之前("Workers sits in front of cache"), +# 所以 **zone 级 Cache Rules / Page Rules 对 Worker 响应完全无效**。 +# 想让 /api/favicon 这类「Worker 直接返回」的响应被边缘缓存、从而不再执行 +# Worker(省 KV 读 + 省 Worker 请求),唯一正解是这个 [cache] enabled。 +# +# 生效条件(缺一不可): +# 1. 这里是 fetch 处理器(scheduled 等不参与缓存) +# 2. 响应带可缓存的 Cache-Control(本项目已带 s-maxage=86400) +# 3. 只缓存 GET/HEAD;带 Set-Cookie 的响应自动绕过 +# 验证:连续两次 curl -I,应依次看到 cf-cache-status: MISS → HIT +# --------------------------------------------------------------------------- +[cache] +enabled = true # --------------------------------------------------------------------------- # D1 (必须) 创建:npx wrangler d1 create artalk-cf