跳到內容

ch06-caching

取得並執行

這個範例可以獨立 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.

Terminal window
npm install && npm run dev
RouteShows
/capabilitieswhat is on caches / Cache / ctx
/probewhich of those methods actually work
/cacheapiCache API put/match — hits from the 2nd request
/namedcaches.open("ch06:custom")
/reservedwhether caches.open("default") throws
/workerscacheWorkers Cache — does nothing locally
/counterhow many times the Worker actually ran

2026-07-28 · wrangler@4.114.0 · workerd@1.20260722.1

Terminal window
$ curl -s localhost:8787/capabilities
{"cachesProto":["open","delete","match","has","keys","constructor"],
"cacheProto":["add","addAll","delete","match","put","matchAll","keys","constructor"]}
Terminal window
$ 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.

Older docs (Miniflare 2 era) describe "default" as a reserved name that throws. Current workerd returns {"reservedNameThrows":false}.

Terminal window
$ 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"}

With "cache": { "enabled": true } in wrangler.jsonc:

Terminal window
$ 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 devproduction
Cache API put/matchworksworks
caches.open()worksworks
Workers Cacheno-opworks
fetch() cf cache optionsno-op (no zone locally)works
cf-cache-statusabsentpresent

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"] }