跳到內容

ch04-bindings

取得並執行

這個範例可以獨立 clone 執行,不依賴其他章節。

npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch04-bindings
cd ch04-bindings
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

說明

Companion example for docs/04-bindings.md.

Exercises four bindings people rarely see in tutorials — ratelimits, version_metadata, secrets.required — plus the two ways to reach env.

Terminal window
npm install
echo 'API_KEY="local-dev-key"' > .dev.vars
npm run cf-typegen
npm run dev

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

The two env objects are not the same object

Section titled “The two env objects are not the same object”
Terminal window
$ curl -s localhost:8787/identity
{"sameObject":false,
"topLevelKeys":["API_KEY","APP_NAME","CACHE","LIMITER","PUBLIC_URL","VERSION"],
"handlerKeys": ["API_KEY","APP_NAME","CACHE","LIMITER","PUBLIC_URL","VERSION"]}

Same keys, different identity. import { env } from "cloudflare:workers" is not an alias for the handler parameter.

process.env exists only with nodejs_compat — and carries secrets

Section titled “process.env exists only with nodejs_compat — and carries secrets”

Default config (flag off):

Terminal window
$ curl -s localhost:8787/procenv
{"hasProcess":false,"appName":null,"apiKey":null}

Uncomment "compatibility_flags": ["nodejs_compat"] in wrangler.jsonc:

Terminal window
$ curl -s localhost:8787/procenv
{"hasProcess":true,"appName":"ch04-bindings","apiKey":"<present>"}

API_KEY is a secret from .dev.vars, and it is now readable by any dependency in the bundle. Enabling nodejs_compat changes your secret exposure surface.

{ "simple": { "limit": 5, "period": 10 } }:

Terminal window
$ for i in $(seq 7); do curl -s -o /dev/null -w "%{http_code} " localhost:8787/limit; done
200 200 200 200 200 429 429

No KV, no Durable Object, no external service.

Terminal window
$ curl -s localhost:8787/version
{"version":{"id":"19cc8aac-1016-4c42-9f66-acc9d6283610","tag":"",
"timestamp":"2026-07-28T04:56:31.361Z"}}

secrets.required generates types on its own

Section titled “secrets.required generates types on its own”

With .dev.vars deleted, wrangler types still emits:

interface __BaseEnv_CloudflareBindings {
CACHE: KVNamespace;
LIMITER: RateLimit;
VERSION: WorkerVersionMetadata;
APP_NAME: "ch04-bindings";
PUBLIC_URL: "https://example.com";
API_KEY: string; // from secrets.required alone
}

Not verified here: enforcement at real deploy time. --dry-run does not check it, since that requires querying the account’s stored secrets.

A missing binding is undefined, not an error

Section titled “A missing binding is undefined, not an error”
Terminal window
$ curl -s localhost:8787/missing
{"value":null}

Combined with named environments not inheriting bindings (chapter 02), this is the most common cause of a staging Worker that deploys green and then throws on its first real request.

Not the docs — Wrangler’s own schema:

Terminal window
jq '.properties | keys' node_modules/wrangler/config-schema.json

原始碼

wrangler.jsonc

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "ch04-bindings",
  "main": "src/index.ts",
  "compatibility_date": "2026-07-24",
  "compatibility_flags": ["nodejs_compat"],
  "observability": { "enabled": true },
  "vars": { "APP_NAME": "ch04-bindings", "PUBLIC_URL": "https://example.com" },
  "kv_namespaces": [{ "binding": "CACHE", "id": "0000000000000000000000000000aaaa" }],
  "version_metadata": { "binding": "VERSION" },
  "ratelimits": [{ "name": "LIMITER", "namespace_id": "1001", "simple": { "limit": 5, "period": 10 } }],
  "secrets": { "required": ["API_KEY"] }
}

package.json

{ "name": "ch04-bindings", "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

import { env as topLevelEnv } from "cloudflare:workers";

// `process` only exists when nodejs_compat is enabled. Probe it safely.
const proc = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;

export default {
  async fetch(request: Request, env: CloudflareBindings, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    switch (url.pathname) {
      case "/identity":
        return Response.json({
          sameObject: (topLevelEnv as unknown) === (env as unknown),
          topLevelKeys: Object.keys(topLevelEnv).sort(),
          handlerKeys: Object.keys(env).sort(),
        });

      case "/procenv":
        return Response.json({
          hasProcess: proc !== undefined,
          appName: proc?.env?.APP_NAME ?? null,
          apiKey: proc?.env?.API_KEY ? "<present>" : null,
        });

      case "/version":
        return Response.json({ version: env.VERSION });

      case "/limit": {
        const { success } = await env.LIMITER.limit({ key: "demo" });
        return Response.json({ success }, { status: success ? 200 : 429 });
      }

      case "/missing":
        return Response.json({
          value: (env as unknown as Record<string, unknown>).DOES_NOT_EXIST ?? null,
        });

      default:
        return new Response("try /identity /procenv /version /limit /missing\n", { status: 404 });
    }
  },
} satisfies ExportedHandler<CloudflareBindings>;

.dev.vars.example

API_KEY="local-dev-key"

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