☰
@cloudflare/workers-response-store 深入解析:在 Cloudflare Workers 上构建程序化响应缓存
2026/9/25 10:22:07 网站建设 项目流程
  • 后端
  • Web框架
  • SSR

【免费下载链接】vinext

Vite plugin that reimplements the Next.js API surface — deploy anywhere

项目地址:https://gitcode.com/gh_mirrors/vi/vinext
点击查看免费下载

@cloudflare/workers-response-store是一个与框架无关的程序化响应存储库,用于在 Cloudflare Workers 中构建持久化响应缓存、stale-while-revalidate(SWR)过期再验证、按需刷新与清理(purge)能力。它把 Workers Cache(边缘交付)、R2(响应体存储)与 SQLite Durable Object(强一致的元数据、版本与标签失效)组合成一个统一的编程接口,既可以作为独立的缓存 Worker 通过 service binding 被多个应用复用,也可以与业务逻辑共存于同一个 Worker 中。读完本文,你将掌握两种部署模式的完整配置、fetch/put/refresh/purge四大 API 的语义、缓存键与新鲜度规则,以及版本保留、分片扩展等运维要点。

本文以 packages/workers-response-store/README.md 为骨架,并补充仓库源码中的实现细节作为佐证。

背景:为什么需要程序化响应缓存

Cloudflare 的原生 HTTP 缓存由缓存规则驱动,而很多框架(如 Next.js 适配器、自定义 SSR 服务)需要在代码层面精确控制"哪些响应可以缓存、如何失效、过期后如何重建"。Workers Response Store 正是为这类场景设计的:

  • 响应持久化:把渲染结果连同响应头、状态码完整写入 R2,即使边缘缓存被逐出也能从 R2 重建;
  • 强一致元数据:用 SQLite Durable Object 记录每个缓存键的活跃版本、新鲜度窗口与标签,保证并发写入、清理与再验证之间的顺序一致;
  • 程序化控制:提供fetch / put / refresh / purge等显式方法,让框架适配层可以自主决定缓存策略,而不是依赖隐式的 CDN 规则。

在仓库中,这个包位于 packages/workers-response-store,其 package.json 声明了主入口dist/index.js与./service服务入口,并提供单 Worker 与 service-binding 两套可运行示例(example/目录)。

安装与前置条件

npm install @cloudflare/workers-response-store

使用前需要准备以下环境(示例配置均采用当前兼容日期):

  • compatibility date 与 flag:配置nodejs_compat兼容标志(如"compatibility_date": "2026-09-16");
  • R2 存储桶:部署前先创建配置中指定的 bucket(示例中为CACHE_BODIES绑定);
  • 生成类型:在 Wrangler 配置完成后运行wrangler types,生成Env类型(包含 R2、Durable Object、service binding 与版本元数据绑定);
  • 可观测性:示例配置开启observability,便于观察缓存命中率与再验证行为。

两种部署模式总览

模式适用场景特点
Service-binding 模式独立应用,或多个 Worker 共享同一套缓存基础设施缓存 Worker 独立部署与观测,存储绑定不进入应用 Worker,后续可无感复用同一缓存服务
单 Worker 模式一个部署、一份 Wrangler 配置更合适应用、缓存入口、R2 与元数据 Durable Object 全部放在同一部署中

无论哪种模式,二者暴露的读、写、刷新、清理 API 完全一致(对应源码中的WorkersResponseStore接口,见 src/binding.ts)。

模式一:Service-binding 部署

Service-binding 模式把缓存基础设施与应用代码分离:缓存 Worker 独享 Workers Cache、R2 与 SQLite Durable Object;每个应用 Worker 各自持有自己的重新生成(regenerate)回调,并把该"版本钉住"的能力通过 RPC 传递给缓存 Worker(对应 src/index.ts 中从CF_VERSION_METADATA.id读取版本号并构造 invocation 的实现)。

1. 缓存 Worker

将main指向安装包自带的服务入口dist/service.js:

{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "response-store", "main": "./node_modules/@cloudflare/workers-response-store/dist/service.js", "compatibility_date": "2026-09-16", "compatibility_flags": ["nodejs_compat"], "workers_dev": false, "cache": { "enabled": true }, "exports": { "default": { "type": "worker", "cache": { "enabled": false } }, "ResponseStoreBinding": { "type": "worker", "cache": { "enabled": true } }, "CacheMetadata": { "type": "durable-object", "storage": "sqlite" }, }, "r2_buckets": [{ "binding": "CACHE_BODIES", "bucket_name": "my-response-bodies" }], "durable_objects": { "bindings": [{ "name": "CACHE_METADATA", "class_name": "CacheMetadata" }], }, "observability": { "enabled": true }, }

关键点解读:

  • exports声明了三个入口:default(未开启缓存,供 RPC 服务入口使用)、ResponseStoreBinding(开启 Workers Cache,实际的读写路径)、CacheMetadata(声明式 SQLite Durable Object);
  • CacheMetadata是声明式导出,不要再把它加进 Wrangler 的migrations——这一点在 README 与示例 example/service-binding/wrangler.cache.jsonc 中均有强调;
  • 存储绑定(R2 bucket、Durable Object namespace)全部留在缓存 Worker 侧,应用 Worker 无需接触存储。

2. 应用 Worker

把应用绑定到未开启缓存的ResponseStoreService入口,并附带版本元数据:

{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-app", "main": "src/worker.ts", "compatibility_date": "2026-09-16", "compatibility_flags": ["nodejs_compat"], "services": [ { "binding": "RESPONSE_STORE", "service": "response-store", "entrypoint": "ResponseStoreService", }, ], "version_metadata": { "binding": "CF_VERSION_METADATA" }, "observability": { "enabled": true }, }

version_metadata绑定是必需项——源码 src/binding.ts 中getVersionId()明确要求存在版本 ID,否则抛出"Workers Response Store requires a version_metadata binding"。版本 ID 用于把 R2 对象与元数据 Durable Object 按应用版本隔离(见后文"版本保留与清理")。

3. 创建客户端并导出入口

import { createWorkersResponseStoreClient } from "@cloudflare/workers-response-store"; const responseStore = createWorkersResponseStoreClient<Env>({ regenerate(input, { env, ctx }) { return render(input.request, env, ctx, input); }, }); export const { ResponseStoreRevalidator, ResponseStoreClient } = responseStore.entrypoints;

createWorkersResponseStoreClient会生成两个入口:ResponseStoreRevalidator(承载应用自己的 regenerate 回调)与ResponseStoreClient(把fetch/put/refresh/purge通过 RPC 转发给缓存 Worker 的ResponseStoreService)。仓库中的完整示例见 example/service-binding/user-worker.ts,对应的两份 Wrangler 配置分别为 wrangler.cache.jsonc 与 wrangler.user.jsonc。

部署顺序为先部署缓存 Worker,再部署应用 Worker——没有反向 service binding,也不存在循环部署。

模式二:单 Worker 部署

单 Worker 模式适合"一个部署、一份配置"足够用的场景。下面三个步骤构成最小集成闭环。

1. 创建并导出 store

import { createWorkersResponseStore } from "@cloudflare/workers-response-store"; const responseStore = createWorkersResponseStore<Env>({ regenerate(input, { env, ctx }) { return render(input.request, env, ctx, { id: input.id, args: input.args, reason: input.reason, }); }, }); export const { CacheMetadata, ResponseStoreRevalidator, ResponseStoreBinding } = responseStore.entrypoints;
  • 命名导出必须与 Wrangler 配置中的声明一致(CacheMetadata、ResponseStoreRevalidator、ResponseStoreBinding);
  • regenerate会在四种场景被调用:SWR 过期、条目硬过期、R2 响应体缺失、显式刷新;
  • 每个存储的响应都要附带一个可序列化的 revalidator 描述符({ id, args }),回调才能在未来重建它。SerializableValue类型(null | boolean | number | string | 数组 | 对象)定义在 src/binding.ts。

2. 读穿回填

function isResponseStoreMiss(response: Response): boolean { return response.status === 404 && response.headers.get("X-Workers-Response-Store") === "MISS"; } export default { async fetch(request, env, ctx) { if (request.method !== "GET") { return render(request, env, ctx); } // Cache identity is pathname + query string. Use a canonical GET request // with only information that is safe to share between visitors. const cacheRequest = new Request(request.url); const cached = await responseStore.fetch(cacheRequest); if (!isResponseStoreMiss(cached)) { return cached; } const response = await render(request, env, ctx); await responseStore.put(cacheRequest, response.clone(), { revalidator: { id: "page", args: [] }, }); return response; }, } satisfies ExportedHandler<Env>;

这是最小集成循环:先fetch读缓存,命中直接返回;未命中(404 +X-Workers-Response-Store: MISS)则渲染并把结果put进去。框架可以在此基础上叠加自己的可缓存性规则、vary 维度、流式策略、错误处理,以及从路由到 revalidator id/args 的映射。

3. 配置 Wrangler

{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-app", "main": "src/worker.ts", "compatibility_date": "2026-09-16", "compatibility_flags": ["nodejs_compat"], "cache": { "enabled": true }, "exports": { "default": { "type": "worker", "cache": { "enabled": false } }, "ResponseStoreBinding": { "type": "worker", "cache": { "enabled": true } }, "CacheMetadata": { "type": "durable-object", "storage": "sqlite" }, }, "r2_buckets": [{ "binding": "CACHE_BODIES", "bucket_name": "my-response-bodies" }], "durable_objects": { "bindings": [{ "name": "CACHE_METADATA", "class_name": "CacheMetadata" }], }, "version_metadata": { "binding": "CF_VERSION_METADATA" }, "observability": { "enabled": true }, }

同样地:CacheMetadata是声明式 SQLite Durable Object 导出,不要重复加入migrations;部署前创建 R2 bucket,配置完成后运行wrangler types生成Env。

仓库中的可运行单 Worker 示例见 example/worker.ts,其regenerate实现还展示了 revalidator 如何读取input.args中的参数(如delayMs、failOnce、cacheControl)来控制再验证行为。

完整 API 参考

createWorkersResponseStore()与createWorkersResponseStoreClient()返回的对象实现了同一套 API:

方法行为
fetch(request)读取规范化的GET缓存键。返回存储的响应或404缓存未命中。SWR 窗口内的过期条目立即返回并在后台重新生成;硬过期条目等待重新生成完成。
put(request, response, options?)在规范化GET缓存键下存储响应。options.revalidator提供{ id, args }供未来重新生成;当替换可能已存在于 Workers Cache 中的条目时,设置purgeExisting: true。
refresh({ tags, pathPrefixes })重新生成匹配条目并清理其先前的边缘响应。至少需要一个选择器。
purge({ tags, pathPrefixes, purgeEverything })移除匹配元数据、记录标签失效、删除响应体并清理对应边缘响应。至少需要一个选择器或purgeEverything: true。
getTagExpiration(tags)返回一组框架管理的软标签的最新失效时间戳。大多数集成不需要这个底层方法。

变更方法统一返回:

type ResponseStoreMutationResult = { backingStoreUpdated: boolean; edgePurgeAccepted: boolean; };

该类型定义见 src/binding.ts。另外put还有一个内部选项coalesce(见 ResponseStorePutOptions),用于合并同一缓存键的并发框架写入。

缓存键规则

  • 键必须是GET请求(源码 deriveCacheKey 会直接抛出"Workers Response Store keys must be GET requests");
  • 身份由URL pathname + query string组成,scheme 与 host 被忽略,随后对cacheKey做 SHA-256 得到keyHash用于分片路由与对象命名;
  • 默认情况下,每个键的响应与读取元数据一起存于单个版本作用域的 R2 对象中,因此新鲜命中与未命中不会查询元数据 Durable Object,无需特殊键格式;
  • 键应基于可信路由与 vary 数据构建,不要包含任意访问者请求头或无界输入,除非你刻意创建独立的共享响应。

新鲜度与标签

新鲜度按优先级从Cloudflare-CDN-Cache-Control→CDN-Cache-Control→Cache-Control推导。这一优先级顺序与实现一一对应:src/cache-policy.ts 的deriveCachePolicy()依次读取这三个头。实现细节还包括:

  • no-store/private直接禁止缓存存储,s-maxage/must-revalidate/proxy-revalidate禁止过期服务(cache-policy.ts);
  • max-age优先取s-maxage,其次max-age,并减去传入的Age值作为初始年龄;
  • stale-while-revalidate决定 SWR 窗口:swrUntil = freshUntil + staleWhileRevalidate * 1000;
  • store 会保留传入的Age值并在存储期间持续推进它(representationAge)。

在传给put()的响应上设置Cache-Tag头(逗号分隔)即可关联清理标签。refresh()与purge()也接受 pathname 前缀(pathPrefixes)。标签失效时大小写不敏感匹配。

regenerate回调收到存储的id与args、一个规范化缓存键请求,以及下列 reason 之一:

Reason触发时机
swr过期响应在其 SWR 窗口内被返回。
expired响应已越过 SWR 窗口,读取必须等待。
missingR2 对象存在,但其已提交的响应内容不可用。
manualrefresh()选中了该条目。

RevalidationReason联合类型定义见 src/binding.ts。

协议响应头

fetch()返回的响应包含以下 Response Store 协议头:

头值含义
X-Workers-Response-StoreMISS、BLOB-FRESH或BLOB-STALE绑定读取时元数据缺失,或 R2 响应处于新鲜/过期状态。未命中是带Cache-Control: no-store的404。
X-Workers-Response-Store-Revision正整数返回给调用方的当前活跃存储版本。未命中时缺失。
X-Workers-Response-Store-Binding-InvocationUUID标识从 R2 加载响应的那次绑定调用。Workers Cache 会复用其缓存填充时的值,可用于诊断绑定是否实际运行。未命中时缺失。
X-Workers-Response-Store-Age-Basis<created-at-ms>:<initial-age-seconds>响应创建时间戳与其原始Age。当前年龄按initialAgeSeconds + max(0, floor((Date.now() - createdAtMs) / 1000))计算。

这些头的生成逻辑见 createStoredResponse 与 MISS_HEADERS。注意:这些头描述的是 Response Store 绑定的结果,而非当前 Workers Cache 边缘状态——边缘状态请查看CF-Cache-Status。适配层可以在返回应用响应前消费并移除这些协议头。

性能设计

Workers Cache 命中即短路

Workers Cache 命中会完全绕过Response Store 绑定 Worker、R2 与所有元数据 Durable Object。只有当 Workers Cache 因未命中或更新而调用绑定时,backing store 与分片路径才会运行。这正是exports中ResponseStoreBinding开启cache而default关闭的原因。

元数据分片(opt-in)

const responseStore = createWorkersResponseStore<Env>({ shards: 16, regenerate, });
  • shards必须是大于 1 的整数,校验逻辑见 validateResponseStoreShards(Number.isSafeInteger(shards) && shards > 1,否则抛TypeError);
  • 未设置时,所有版本作用域元数据使用最初的单个 Durable Object;
  • 每个缓存键及其全部版本、claims、pending 对象都确定性路由到单个分片(基于keyHash前 8 位十六进制取模,见 getMetadata);
  • refresh与purge在所有分片间扇出;标签失效时间戳会被复制到每个分片,从而保证 publication fencing 与软标签检查仍然正确;软标签读取会选中稳定副本以分散负载(getTagMetadata);
  • 改变分片数量会选定新的元数据与 R2 布局,应视为"缓存冷启动式"部署变更,而不是原地扩缩容。

元数据位置提示

两种部署模式都接受 Durable Object 位置提示:

const responseStore = createWorkersResponseStore<Env>({ locationHint: "weur", regenerate, });

service-binding 模式下对createWorkersResponseStoreClient()使用相同的locationHint选项。合法的取值(源码 RESPONSE_STORE_LOCATION_HINTS 定义并校验)包括:afr、apac、apac-ne、apac-se、eeur、enam、me、oc、sam、weur、wnam。

该提示是best-effort的,只影响每个元数据 Durable Object 的首次创建。已有对象在选项改变时永不迁移,因此应把修改后的提示作为缓存冷启动部署推出,并使其与 R2 bucket 的位置一起规划。

存储与运维行为

一次请求的完整路径如下:

application Worker └─ ResponseStoreBinding (Workers Cache enabled) ├─ cache hit ───────────────► stored Response └─ cache miss ├─ response + read metadata ─► R2 ├─ write coordination/revisions ─► SQLite Durable Object └─ regeneration ───────► application callback

R2 对象布局与一致性

  • 响应使用runtime-cache/<version-id>/r2-v1/[shards-<count>/]<digest>/active布局(objectKeyRoot 与 r2ObjectKey)。布局段(r2-v1)将本实现与 service-binding 回滚、滚动部署期间更旧的缓存 Worker 版本隔离;
  • SQLite 修订号与条件发布防止慢写入覆盖新写入或"复活"已清理条目。用户 RPC、R2 与缓存清理 I/O 都在 SQLite 事务之外执行;
  • 清理以更高修订号的墓碑替换活跃 R2 对象。后续 put 可以替换该墓碑,但更旧的延迟写入无法重建已清理内容;
  • R2 墓碑在 SQLite 中持久排队,以有界批次排空。失败的 R2 操作会保留其墓碑排队,后续清理可重试,而不会丢失防复活围栏(anti-resurrection fence);
  • SWR 窗口内的过期 R2 响应会立即返回,同时ctx.waitUntil()运行一次带 claim 的再验证;后续的 Workers Cache 请求会提升该已完成修订,因此可能多出现一次过期响应;
  • 硬过期响应永远不会被返回:读取会等待再验证,因此要求存在存储的 revalidator 描述符;
  • 被放弃的写预留保留一小时,之后由 alarm 驱动的清理将它们围栏化,防止后续发布。响应体只在 Durable Object 发布后写入 R2,因此该清理不执行 R2 操作;
  • RPC 传输的响应体会在 R2 写入前缓冲:转移的流不保留 R2 单部分 put API 所需的定长标记(源码 storeResponse 中的注释明确说明)。选择最大响应尺寸时要考虑 Worker 内存限制;
  • service-binding 回调始终钉在提供 revalidator 能力时的应用 Worker 版本上。

SQLite 侧的表结构(entries、entry_tags、revalidation_claims、tag_invalidations、pending_r2_tombstones等)在 src/metadata-do.ts 的CacheMetadata构造函数中创建。

版本保留与清理

backing 数据按应用 Worker 版本 ID隔离:新部署会以冷 backing 布局启动,同时保留旧版本以支持回滚。部署新版本不会删除旧版本的 R2 对象或元数据 Durable Object,且库当前不执行跨版本垃圾回收。

这是"部署兼容",而非原地对象布局迁移:

  • 单 Worker 模式:新版本冷启动,旧版本数据保留供回滚;
  • service-binding 模式:只升级缓存 Worker 时,现有应用版本 ID 保持不变,但旧 R2 布局中的条目被视为冷未命中,现有客户端通过不变 API 重新填充,旧缓存键格式仍是合法普通键。旧 R2 对象在该应用版本被清理前一直保留。不要把同一应用版本 ID 手动指派给无关部署。

保留每个仍可能接收流量或可回滚到的版本。当某版本永久退役时:

  1. 若它仍可寻址,通过该版本调用purge({ purgeEverything: true })墓碑化其元数据与活跃 R2 响应对象;这同时会请求宽泛的边缘缓存清理,因此其他版本可能需要重新填充;
  2. 删除其 R2 前缀下的所有剩余对象:runtime-cache/<version-id>/包含该应用版本的每个存储布局与分片数量;
  3. 若需回收已退役元数据 Durable Object 的 SQLite 存储,通过应用自有的管理路径对每个已知对象调用deleteAll()。当前布局名为<version-id>:r2-v1(无分片)或<version-id>:r2-v1:metadata-shard:<index>-of-<count>(每个分片一个)。更旧布局可能还有额外对象。该清理不属于包 API。

注意:不要在使用中的版本还共享命名空间时删除 Durable Object namespace;除非能保证存活时间超过每个有效响应与回滚窗口,否则避免仅按年龄的 R2 生命周期规则——显式的退役版本前缀可以避免误删仍活跃的旧缓存条目。

结语

@cloudflare/workers-response-store提供了一条从"边缘缓存"到"程序化响应存储"的完整路径:Workers Cache 负责最快的命中路径,R2 提供持久化的响应体,SQLite Durable Object 提供强一致的元数据、修订、墓碑与标签失效。无论选择单 Worker 的自包含部署,还是 service-binding 的独立缓存基础设施,统一的fetch/put/refresh/purgeAPI 都能让框架适配层精确控制缓存生命周期,同时通过shards与locationHint在扩展性与地域亲和性之间取得平衡。

可运行的完整集成示例位于本包的 example/ 目录(单 Worker 的 worker.ts 与 service-binding 的 user-worker.ts 及其两份 wrangler 配置),对应的端到端测试可参考 tests/e2e.test.ts 与 tests/service-binding-e2e.test.ts,它们是理解完整行为契约的最佳入口。

  • 后端
  • Web框架
  • SSR

【免费下载链接】vinext

Vite plugin that reimplements the Next.js API surface — deploy anywhere

项目地址:https://gitcode.com/gh_mirrors/vi/vinext
点击查看免费下载
上一篇:如何快速备份微博:3步完成完整PDF导出的终极指南
下一篇:DDrawCompat:让经典游戏在现代Windows上完美运行的终极兼容方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询