- 后端
- Web框架
- SSR
【免费下载链接】vinext
Vite plugin that reimplements the Next.js API surface — deploy anywhere
@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 窗口,读取必须等待。 |
missing | R2 对象存在,但其已提交的响应内容不可用。 |
manual | refresh()选中了该条目。 |
RevalidationReason联合类型定义见 src/binding.ts。
协议响应头
fetch()返回的响应包含以下 Response Store 协议头:
| 头 | 值 | 含义 |
|---|---|---|
X-Workers-Response-Store | MISS、BLOB-FRESH或BLOB-STALE | 绑定读取时元数据缺失,或 R2 响应处于新鲜/过期状态。未命中是带Cache-Control: no-store的404。 |
X-Workers-Response-Store-Revision | 正整数 | 返回给调用方的当前活跃存储版本。未命中时缺失。 |
X-Workers-Response-Store-Binding-Invocation | UUID | 标识从 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 callbackR2 对象布局与一致性
- 响应使用
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 手动指派给无关部署。
保留每个仍可能接收流量或可回滚到的版本。当某版本永久退役时:
- 若它仍可寻址,通过该版本调用
purge({ purgeEverything: true })墓碑化其元数据与活跃 R2 响应对象;这同时会请求宽泛的边缘缓存清理,因此其他版本可能需要重新填充; - 删除其 R2 前缀下的所有剩余对象:
runtime-cache/<version-id>/包含该应用版本的每个存储布局与分片数量; - 若需回收已退役元数据 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
相关推荐
深入解析 EmDash blog-cloudflare 模板:在 Astro 上构建部署于 Cloudflare Workers 的 CMS 博客
深入解析 EmDash blog cloudflare 模板:在 Astro 上构建部署于 Cloudflare Workers 的 CMS 博客 导读 tem
CMS后端前端插件系统Vike 云函数实战:在 Cloudflare Workers 上构建 React 流式 SSR 应用(examples/cloudflare-workers-react-full 详解)
Vike 云函数实战:在 Cloudflare Workers 上构建 React 流式 SSR 应用(examples/cloudflare workers
前端后端Web框架SSR在 Cloudflare Workers 上托管 Rivet Actors:@rivetkit/cloudflare-workers 实战指南
在 Cloudflare Workers 上托管 Rivet Actors:@rivetkit/cloudflare workers 实战指南 Rivet Ac
后端AI Agent人工智能流程编排WebSocket
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考