ch06-caching
對應 06. 快取:fetch()、Cache API、Workers Cache 三層
在 GitHub 上檢視·4 個檔案·5.7 KB
取得並執行
這個範例可以獨立 clone 執行,不依賴其他章節。
npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch06-caching
cd ch06-caching
npm install可用指令
npm run dev # wrangler dev
npm run deploy # wrangler deploy
npm run typecheck # tsc --noEmit
npm run cf-typegen # wrangler types --env-interface CloudflareBindings說明
ch06 — Three caching layers side by side
Section titled “ch06 — Three caching layers side by side”Companion example for docs/06-caching.md.
npm install && npm run dev| Route | Shows |
|---|---|
/capabilities | what is on caches / Cache / ctx |
/probe | which of those methods actually work |
/cacheapi | Cache API put/match — hits from the 2nd request |
/named | caches.open("ch06:custom") |
/reserved | whether caches.open("default") throws |
/workerscache | Workers Cache — does nothing locally |
/counter | how many times the Worker actually ran |
Verified findings
Section titled “Verified findings”2026-07-28 · wrangler@4.114.0 · workerd@1.20260722.1
Half of the Cache API exists but throws
Section titled “Half of the Cache API exists but throws”$ curl -s localhost:8787/capabilities{"cachesProto":["open","delete","match","has","keys","constructor"], "cacheProto":["add","addAll","delete","match","put","matchAll","keys","constructor"]}$ curl -s localhost:8787/probe{"caches.has": "THREW: ... 'has' on 'CacheStorage': the method is not implemented.", "caches.keys": "THREW: ... 'keys' on 'CacheStorage': the method is not implemented.", "caches.delete": "THREW: ... 'delete' on 'CacheStorage': the method is not implemented.", "cache.add": "THREW: ... 'add' on 'Cache': the method is not implemented.", "cache.addAll": "THREW: ... 'addAll' on 'Cache': the method is not implemented.", "cache.matchAll": "THREW: ... 'matchAll' on 'Cache': the method is not implemented.", "cache.keys": "THREW: ... 'keys' on 'Cache': the method is not implemented.", "ctx.cache value": "ok: \"undefined\""}So typeof caches.keys === "function" is true and calling it fails.
Feature detection does not work here. Only caches.default, caches.open(),
cache.match(), cache.put() and cache.delete() are real.
The generated types are correct — CacheStorage has no has/keys/delete
and Cache has no add/addAll/matchAll/keys, so calling them needs a
cast. worker-configuration.d.ts is again the better reference.
caches.open("default") does not throw
Section titled “caches.open("default") does not throw”Older docs (Miniflare 2 era) describe "default" as a reserved name that
throws. Current workerd returns {"reservedNameThrows":false}.
Cache API works locally
Section titled “Cache API works locally”$ for i in 1 2 3; do curl -s localhost:8787/cacheapi; echo; done{"layer":"cache-api","hit":false,"body":"expensive-value-1"}{"layer":"cache-api","hit":true,"body":"expensive-value-1"}{"layer":"cache-api","hit":true,"body":"expensive-value-1"}Workers Cache does NOT work locally
Section titled “Workers Cache does NOT work locally”With "cache": { "enabled": true } in wrangler.jsonc:
$ for i in 1 2 3; do curl -s localhost:8787/workerscache; echo; done{"layer":"workers-cache","n":2}{"layer":"workers-cache","n":3}{"layer":"workers-cache","n":4}The counter keeps climbing — the Worker ran every time. No cf-cache-status
header, and ctx.cache is undefined.
wrangler dev | production | |
|---|---|---|
Cache API put/match | works | works |
caches.open() | works | works |
| Workers Cache | no-op | works |
fetch() cf cache options | no-op (no zone locally) | works |
cf-cache-status | absent | present |
Deploy and check cf-cache-status before trusting any caching strategy.
原始碼
wrangler.jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "ch06-caching",
"main": "src/index.ts",
"compatibility_date": "2026-07-24",
"observability": { "enabled": true },
"cache": { "enabled": true }
}package.json
{ "name": "ch06-caching", "private": true, "type": "module",
"scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", "typecheck": "tsc --noEmit",
"cf-typegen": "wrangler types --env-interface CloudflareBindings" },
"devDependencies": { "typescript": "^5.9.0", "wrangler": "^4.114.0" } }src/index.ts
// ---------------------------------------------------------------------------
// Chapter 06 — three caching layers, side by side.
// ---------------------------------------------------------------------------
let originHits = 0;
export default {
async fetch(request: Request, env: CloudflareBindings, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
switch (url.pathname) {
// --- Layer 2: Cache API, explicit put/match ------------------------
case "/cacheapi": {
const cache = caches.default;
const key = new Request(new URL("/cacheapi-key", url).toString(), { method: "GET" });
const hit = await cache.match(key);
if (hit) {
const body = await hit.text();
return Response.json({ layer: "cache-api", hit: true, body });
}
originHits++;
const fresh = new Response(`expensive-value-${originHits}`, {
headers: { "cache-control": "public, max-age=60" },
});
ctx.waitUntil(cache.put(key, fresh.clone()));
return Response.json({ layer: "cache-api", hit: false, body: await fresh.text() });
}
// --- What CacheStorage methods actually exist? ----------------------
case "/capabilities": {
const c = caches.default;
const proto = Object.getPrototypeOf(c);
return Response.json({
cachesOwn: Object.getOwnPropertyNames(caches),
cachesProto: Object.getOwnPropertyNames(Object.getPrototypeOf(caches)),
cacheProto: Object.getOwnPropertyNames(proto),
hasCtxCache: "cache" in ctx,
ctxCacheProto: ctx.cache
? Object.getOwnPropertyNames(Object.getPrototypeOf(ctx.cache))
: null,
});
}
// --- Named cache: does caches.open work? "default" is reserved -----
case "/named": {
try {
const named = await caches.open("ch06:custom");
const k = new Request(new URL("/named-key", url).toString());
const hit = await named.match(k);
if (hit) return Response.json({ named: true, hit: true, body: await hit.text() });
await named.put(k, new Response("named-value", {
headers: { "cache-control": "public, max-age=60" },
}));
return Response.json({ named: true, hit: false });
} catch (e) {
return Response.json({ named: false, error: String(e) }, { status: 500 });
}
}
case "/reserved": {
try {
await caches.open("default");
return Response.json({ reservedNameThrows: false });
} catch (e) {
return Response.json({ reservedNameThrows: true, error: String(e) });
}
}
// --- Layer 3: Workers Cache — response headers drive it ------------
// With `cache.enabled = true` this response should be cached in front
// of the Worker, so the counter stops moving on a HIT.
case "/workerscache": {
originHits++;
return new Response(JSON.stringify({ layer: "workers-cache", n: originHits }), {
headers: {
"content-type": "application/json",
"cache-control": "public, max-age=60",
},
});
}
// Do the "documented as missing" methods actually work?
case "/probe": {
const c = caches.default;
const k = new Request(new URL("/probe-key", url).toString());
const out: Record<string, string> = {};
const t = async (name: string, fn: () => Promise<unknown>) => {
try { out[name] = "ok: " + JSON.stringify(await fn()); }
catch (e) { out[name] = "THREW: " + String(e).slice(0, 120); }
};
// These exist on the prototype at runtime but are absent from the
// generated types, so they need a cast to call at all.
const anyCaches = caches as unknown as Record<string, (...a: never[]) => Promise<unknown>>;
const anyCache = c as unknown as Record<string, (...a: never[]) => Promise<unknown>>;
await t("caches.has", () => anyCaches.has("ch06:custom" as never));
await t("caches.keys", () => anyCaches.keys());
await t("caches.delete", () => anyCaches.delete("ch06:nope" as never));
await t("cache.add", () => anyCache.add(k as never));
await t("cache.addAll", () => anyCache.addAll([k] as never));
await t("cache.matchAll", () => anyCache.matchAll(k as never));
await t("cache.keys", () => anyCache.keys());
await t("ctx.cache value", async () => (ctx.cache === undefined ? "undefined" : typeof ctx.cache));
return Response.json(out);
}
case "/counter":
return Response.json({ originHits });
default:
return new Response(
"try /cacheapi /named /reserved /capabilities /workerscache /counter\n",
{ status: 404 },
);
}
},
} satisfies ExportedHandler<CloudflareBindings>;tsconfig.json
{ "compilerOptions": { "target": "esnext", "lib": ["esnext"], "module": "esnext",
"moduleResolution": "bundler", "types": ["./worker-configuration.d.ts"],
"strict": true, "skipLibCheck": true, "noEmit": true, "isolatedModules": true },
"include": ["src/**/*.ts", "worker-configuration.d.ts"] }