工具鏈:Wrangler v4、`wrangler.jsonc` 與本機開發
這篇要解決的問題
Section titled “這篇要解決的問題”上一篇談的是「Workers 是什麼」。這一篇要建立你之後 41 篇都會用到的工作環境,並且釐清三件會反覆咬人的事:
compatibility_date不是樣板欄位。 它是 runtime 的版本鎖。同一份原始碼、只改這個日期,我實測到 bundle 大小差了 117 倍。wrangler types產生的型別會隨設定改變。 它不只是@cloudflare/workers-types的替代品,它會把你的vars變成 literal type、把.dev.vars的 secret 也算進去。- 本機跑的是真的 workerd,但不是真的 production。 Wrangler v4 起大部分資料指令預設操作本機,而 v3 時代的教學指令複製過來會靜默地打錯目標。
Wrangler v4 與 wrangler.jsonc
Section titled “Wrangler v4 與 wrangler.jsonc”目前的 CLI 是 Wrangler v4(本文使用 4.114.0)。沒有 v5;v3 的 bugfix 支援已於 2026 年 Q1 結束。
設定檔格式有 wrangler.toml 和 wrangler.jsonc 兩種。官方的說法很直接:
“Cloudflare recommends using
wrangler.jsoncfor new projects, and some newer Wrangler features will only be available to projects using a JSON config file.”
新專案一律用 .jsonc,而且第一行放 $schema,編輯器就有自動補全與驗證:
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2026-07-24", "observability": { "enabled": true }}.jsonc 是「可以寫註解的 JSON」。對設定檔來說這很重要 —— 每個 binding 的 id 旁邊寫一行「這是哪個環境的」,半年後的你會感謝自己。
建立專案用 npm create cloudflare@latest(C3)。⚠️ 但有兩個 template 目前是壞的,後面會遇到:Astro template(第 25 篇)產出已不存在的設定鍵,React Router 的官方 starter(第 24 篇)還停在 v7。C3 對純 Worker 專案是可靠的,對框架 template 要驗證過再用。
compatibility_date:runtime 的版本鎖
Section titled “compatibility_date:runtime 的版本鎖”這是整篇最重要的一節。
Workers 沒有「runtime 版本號」讓你選。取而代之的是 compatibility_date —— 你宣告「我的程式碼是照著這一天的 runtime 行為寫的」,Cloudflare 保證行為不變。新的行為改動一律綁在新日期後面,舊 Worker 永遠不會因為平台更新而壞掉。
聽起來只是相容性保險,但它的實際影響大得多。
實驗:一個日期,bundle 差 117 倍
Section titled “實驗:一個日期,bundle 差 117 倍”同一份原始碼:
import { existsSync } from "node:fs";export default { fetch() { return Response.json({ fs: typeof existsSync }); },} satisfies ExportedHandler;兩份設定,只有日期不同(兩邊都開了 nodejs_compat):
$ wrangler deploy --dry-run --outdir out # compatibility_date: 2025-01-01Total Upload: 25.30 KiB / gzip: 5.88 KiB
$ wrangler deploy --dry-run --outdir out # compatibility_date: 2026-07-24Total Upload: 0.22 KiB / gzip: 0.17 KiB25,903 bytes vs 221 bytes。 打開產出的 bundle 就知道為什麼:
2025-01-01 的版本裡塞滿了 polyfill:
function createNotImplementedError(name) { return new Error(`[unenv] ${name} is not implemented yet!`);}(bundle 裡出現 32 處 unenv 引用。)
2026-07-24 的版本則是:
import { existsSync } from "node:fs";var index_default = { fetch() { return Response.json({ fs: typeof existsSync }); } };export { index_default as default };node:fs 就是 runtime 原生提供的,什麼都不用打包。
原因是 enable_nodejs_fs_module 這個 flag 在 2025-09-15 轉為預設開啟。日期在那之前,Wrangler 只好用 unenv 的 polyfill 幫你補;日期在那之後,直接用 runtime 內建的。
2025–2026 這類「轉為預設開啟」的 flag 是一整串:
| 生效日 | 預設開啟的能力 |
|---|---|
| 2025-08-15 | enable_nodejs_http_modules |
| 2025-09-15 | enable_nodejs_fs_module、os、process_v2 |
| 2026-01-29 | enable_nodejs_sqlite、dgram、inspector |
| 2026-03-17 | child_process、worker_threads、perf_hooks、v8、tty |
| 2026-06-16 | remove_nodejs_compat_eol_v24、throw_on_not_implemented_tls_options |
回想第 1 篇的 script size 限制(Free 3MB / Paid 10MB,壓縮後)—— 一個過時的 compatibility_date 會讓你莫名其妙地更接近那道牆,而錯誤訊息不會告訴你原因出在日期上。
它也會改變型別
Section titled “它也會改變型別”同一個專案,只改日期後重跑 wrangler types:
compatibility_date 2026-07-24 → 14,716 行compatibility_date 2022-01-01 → 14,576 行差的那 140 行是什麼?
> declare abstract class Navigator> declare abstract class ReadableByteStreamController> declare abstract class ReadableStreamBYOBRequest> declare abstract class TransformStreamDefaultController> declare class MessageChannel在 2022-01-01 的世界裡,navigator 和 MessageChannel 在型別層面根本不存在。這不是型別定義偷懶,是那個日期的 runtime 真的沒有這些東西。
- 新專案:用今天的日期。官方文件範例目前用
2026-07-24。 - 既有專案:可以升,但要當成一次真正的升級來做 —— 讀 changelog、跑測試、用 gradual deployment 放量(第 40 篇)。不要順手改。
- 絕對不要照抄別人教學裡的日期。Cloudflare 自己的
/workers/get-started/prompting/頁面到現在還寫死2025-03-07。
compatibility_flags 則是給兩種情況用的:提早開啟還沒到預設日期的功能,或反過來關掉某個已預設的行為。nodejs_compat 是最常見的一個 —— 它至今仍是 opt-in、永遠不會自動開啟,且需要 compatibility_date >= 2024-09-23。
⚠️
nodejs_compat_v2已經摺進nodejs_compat,不要再手動加。 2024 年的教學會叫你兩個都寫,現在官方的 Node.js runtime 文件連提都不提 v2 了。
wrangler types:讓型別從設定檔流出來
Section titled “wrangler types:讓型別從設定檔流出來”2026 年的做法是不要手動安裝 @cloudflare/workers-types。改用:
wrangler types它產生 worker-configuration.d.ts,內容包含兩部分:你的 binding 型別 + 對應 compat date 的完整 runtime 型別。
官方的說法是:@cloudflare/workers-types 沒有被廢棄,但它現在的定位是「給函式庫和共用套件用」,Worker 專案本身應該用 wrangler types。
生成結果比你以為的聰明
Section titled “生成結果比你以為的聰明”拿這份設定:
{ "vars": { "APP_NAME": "ch02-toolchain", "TIER": "dev" }, "kv_namespaces": [{ "binding": "SETTINGS", "id": "...ffff" }], "env": { "staging": { "vars": { "APP_NAME": "ch02-toolchain", "TIER": "staging" }, "kv_namespaces": [{ "binding": "SETTINGS", "id": "...eeee" }] } }}加上一個本機 secret 檔 .dev.vars:
API_KEY="local-dev-key-not-a-real-secret"跑 wrangler types --env-interface CloudflareBindings,得到:
// Generated by Wrangler by running `wrangler types --env-interface=CloudflareBindings`// (hash: 972b0656c629fd41090c84cec8b673b6)// Runtime types generated with workerd@1.20260722.1 2026-07-24
interface __BaseEnv_CloudflareBindings { SETTINGS: KVNamespace; APP_NAME: "ch02-toolchain"; TIER: "staging" | "dev"; API_KEY: string;}
declare namespace Cloudflare { interface StagingEnv { SETTINGS: KVNamespace; APP_NAME: "ch02-toolchain"; TIER: "staging"; API_KEY: string; }}四件值得注意的事:
vars拿到的是 literal type,不是string。APP_NAME: "ch02-toolchain"。- 跨環境的 var 會被 union 起來:
TIER: "staging" | "dev"。所以if (env.TIER === "production")會直接是編譯錯誤 —— 這很好,但如果你不知道會很困惑。 .dev.vars裡的 secret 也會被讀進來,型別是string。這代表.dev.vars不只是本機注入,它同時是你的 secret 清單宣告。- 標頭寫死了
workerd@1.20260722.1 2026-07-24。 型別檔本身就是 compat date 的產物。
--env-interface:預先避開 Hono 的型別衝突
Section titled “--env-interface:預先避開 Hono 的型別衝突”預設產生的介面叫 Env。但 Hono 自己也有一個叫 Env 的型別({ Bindings, Variables } 的容器),兩者會撞。第 5 篇會正式遇到,但現在就養成習慣比較省事:
{ "scripts": { "cf-typegen": "wrangler types --env-interface CloudflareBindings" } }--check:在 CI 擋下過期型別
Section titled “--check:在 CI 擋下過期型別”$ wrangler types --check✘ [ERROR] Types at worker-configuration.d.ts are out of date. Run `wrangler types` to regenerate.它比對的是標頭那個 hash。任何 wrangler.jsonc 的異動都會讓 hash 變掉。把這行加進 CI,可以擋掉「改了 binding 但忘記重跑 typegen,結果 CI 綠燈、production 紅燈」這種事。
worker-configuration.d.ts 建議 不要 commit(放進 .gitignore),把 cf-typegen 掛在 postinstall 或 CI 前置步驟。
本機開發跑的是什麼
Section titled “本機開發跑的是什麼”wrangler dev 啟動的是 workerd —— 和 Cloudflare production 同一份 runtime 二進位(透過 miniflare v4 包裝)。這和「用 Node.js 模擬 Workers API」的舊工具是完全不同的東西,行為一致性高很多。
但不是完全一致。 第 1 篇已經實測到一個分歧(Date.now() 的凍結行為只在 production 生效),第 27 篇還會遇到 PBKDF2 迭代上限的分歧。規則:凡是牽涉安全邊界或資源限制的行為,以部署後實測為準。
v4 的最大行為改變:資料指令 local-first
Section titled “v4 的最大行為改變:資料指令 local-first”$ wrangler kv key put greeting "hello from local KV" --binding SETTINGS
Use --remote if you want to access the remote instance.
Writing the value "hello from local KV" to key "greeting" on namespace binding: "SETTINGS" (id: "0000000000000000000000000000ffff").注意:沒有 --local flag,也完全不需要登入。 資料寫進專案底下的 .wrangler/state/。
這對新手是好事(不會手滑改到 production),但如果你照抄 v3 時代的教學,那些指令現在會靜默地操作本機,你會盯著 production dashboard 想不通資料去哪了。
三種開發模式
Section titled “三種開發模式”| 模式 | 指令 | 資料在哪 | 什麼時候用 |
|---|---|---|---|
| 全本機 | wrangler dev | .wrangler/state/ | 預設。日常開發 |
| 混合(推薦) | wrangler dev + binding 上加 "remote": true | 該 binding 走 production,其餘本機 | 需要真實資料,但其他部分要快速迭代 |
| 全遠端 | wrangler dev --remote | 全部 production | 已是 legacy,盡量別用 |
Remote bindings 在 2025-09-16 隨 Wrangler 4.37.0 正式 GA,是現在的建議做法。設定方式是逐一標記:
{ "kv_namespaces": [ { "binding": "SETTINGS", "id": "<production-id>", "remote": true } ]}wrangler dev 啟動時會印出一張表,最後一欄就是每個 binding 的模式:
Binding Resource Modeenv.SETTINGS (0000000000000000000000000000eeee) KV Namespace localenv.APP_NAME ("ch02-toolchain") Environment Variable localenv.TIER ("staging") Environment Variable localenv.API_KEY ("(hidden)") Environment Variable local⚠️ 舊名
experimental_remote在 GA 時已改為remote。另外preview_id/preview_bucket_name/preview_database_id這些欄位只是為了服務 legacy 的--remote而存在,新專案不需要。
Secrets 與環境
Section titled “Secrets 與環境”本機用 .dev.vars(務必加進 .gitignore):
API_KEY="local-dev-key-not-a-real-secret"Production 用:
wrangler secret put API_KEYwrangler secret bulk .prod.vars # 批次Secret 一旦寫入就讀不回來,只能覆寫。第 41 篇會談到 Secrets Store(帳號層級共用 secret)。
多環境用 env.<name>,部署時 wrangler deploy --env staging。注意這是 Wrangler Environments,不是已棄用的 Service Environments。
🔴 環境不會繼承 vars 和 bindings
Section titled “🔴 環境不會繼承 vars 和 bindings”這是我認為第 2 篇最值得記住的一個坑。把 staging 寫成這樣:
{ "vars": { "APP_NAME": "ch02-toolchain", "TIER": "dev" }, "kv_namespaces": [{ "binding": "SETTINGS", "id": "...ffff" }], "env": { "staging": { "vars": { "TIER": "staging" } } }}直覺上 staging 應該繼承 APP_NAME 和 SETTINGS,只覆寫 TIER。實際上:
▲ [WARNING] Processing wrangler.jsonc configuration: - "env.staging" environment configuration - The following vars exist at the top level, but not on "env.staging.vars". This is probably not what you want, since "vars" configuration is not inherited by environments. Please add these vars to "env.staging.vars": - APP_NAME - "kv_namespaces" exists at the top level, but not on "env.staging". This is not what you probably want, since "kv_namespaces" is not inherited by environments.
Your Worker has access to the following bindings:Binding Resourceenv.TIER ("staging") Environment VariableAPP_NAME 和 SETTINGS 整個消失了。 而且注意這是 WARNING 不是 ERROR —— wrangler deploy --env staging 會成功,然後你的 Worker 在 staging 上因為 env.SETTINGS 是 undefined 而爆炸。
規則:每個 named environment 都必須完整重新宣告自己的 vars 和所有 bindings。 相對地,compatibility_date、compatibility_flags、main 這類頂層設定是會繼承的。
如果你受不了這種重複(我也受不了),第 26 篇會談 monorepo 裡用腳本生成設定的做法。
動手做:把設定接起來
Section titled “動手做:把設定接起來”完整程式碼:
examples/ch02-toolchain/
一支 Worker,同時用到 var、secret、KV binding 與兩個環境。
wrangler.jsonc
Section titled “wrangler.jsonc”{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "ch02-toolchain", "main": "src/index.ts", "compatibility_date": "2026-07-24", "observability": { "enabled": true },
"vars": { "APP_NAME": "ch02-toolchain", "TIER": "dev" }, "kv_namespaces": [ { "binding": "SETTINGS", "id": "0000000000000000000000000000ffff" } ],
// Environments do NOT inherit vars or bindings. // Everything must be repeated here. "env": { "staging": { "vars": { "APP_NAME": "ch02-toolchain", "TIER": "staging" }, "kv_namespaces": [ { "binding": "SETTINGS", "id": "0000000000000000000000000000eeee" } ] } }}KV namespace id 在本機開發是隨便填的 —— 它只被當成本機儲存的分區 key。要接 production 才需要真的 id。
src/index.ts
Section titled “src/index.ts”import { env } from "cloudflare:workers";
// Top-level access to vars and secrets is allowed.// Top-level I/O is NOT — see chapter 01.const APP = env.APP_NAME;
export default { async fetch(request: Request, workerEnv: CloudflareBindings): Promise<Response> { const url = new URL(request.url);
if (url.pathname === "/config") { return Response.json({ appName: APP, tier: workerEnv.TIER, // Never return a secret. We only prove it was injected. hasApiKey: typeof workerEnv.API_KEY === "string" && workerEnv.API_KEY.length > 0, }); }
if (url.pathname === "/settings") { const value = await workerEnv.SETTINGS.get("greeting"); return Response.json({ greeting: value ?? null }); }
return new Response("Try /config or /settings\n", { status: 404 }); },} satisfies ExportedHandler<CloudflareBindings>;npm installecho 'API_KEY="local-dev-key-not-a-real-secret"' > .dev.varsnpm run cf-typegen
# Write to local KV — no login required, no --local flag needednpx wrangler kv key put greeting "hello from local KV" --binding SETTINGS
npm run dev預設環境:
$ curl -s localhost:8787/config{"appName":"ch02-toolchain","tier":"dev","hasApiKey":true}
$ curl -s localhost:8787/settings{"greeting":"hello from local KV"}staging 環境:
$ npx wrangler dev --env staging
$ curl -s localhost:8787/config{"appName":"ch02-toolchain","tier":"staging","hasApiKey":true}
$ curl -s localhost:8787/settings{"greeting":null}greeting 變成 null —— 因為 staging 綁的是不同的 KV namespace id,本機儲存也跟著分開。這正好證明了環境隔離在本機就是真的隔離,不是假裝的。
順手驗證前面講的東西
Section titled “順手驗證前面講的東西”# 1. Types are in sync with confignpx wrangler types --check
# 2. Break it and watch CI-style failure# (edit any var in wrangler.jsonc, then re-run --check)
# 3. Inspect the bundle without deploying — no account needednpx wrangler deploy --dry-run --outdir outls -la out/wrangler deploy --dry-run --outdir 是本系列會反覆用到的工具:不需要 Cloudflare 帳號就能驗證設定、看 bundle 大小、檢查有沒有意外打包進 polyfill。 examples repo 的 CI 就是靠它做第一層把關。
接進 LinkForge
Section titled “接進 LinkForge”這一篇替 LinkForge 建好 monorepo 骨架。用 pnpm workspace:
linkforge/├── pnpm-workspace.yaml├── package.json├── apps/│ ├── redirector/ # hot path — deliberately minimal│ ├── api/ # Hono API + DO + Workflows│ ├── dashboard/ # React Router v8 (ch24)│ ├── site/ # Astro (ch25)│ └── consumers/ # Queue consumers (ch19)├── packages/│ ├── db/ # Drizzle schema + migrations (ch10)│ ├── shared/ # shared types + zod schemas│ └── config/ # base tsconfig / eslint└── examples/ # per-chapter standalone demos三個現在就定下來的約定:
① 每個 app 有自己的 wrangler.jsonc 與自己的 cf-typegen。 Binding 型別是 per-Worker 的,不要試圖共用一份 Env。
{ "scripts": { "cf-typegen": "wrangler types --env-interface CloudflareBindings", "check:types": "wrangler types --check && tsc --noEmit" }}② compatibility_date 全 repo 統一。 寫進根目錄的 VERSIONS.md,升級時一起升、一起測。不同 Worker 用不同日期會讓「這個 API 在 A 有在 B 沒有」變成日常。
③ .gitignore 一開始就寫好:
node_modules/.wrangler/dist/.dev.vars.dev.vars.*worker-configuration.d.ts.dev.vars 和 worker-configuration.d.ts 是重點 —— 前者是 secret,後者是生成物(且會隨 compat date 變動,commit 進去只會製造 diff 噪音)。
本篇交付物:monorepo 骨架 + 一支能跑的 apps/redirector(硬編碼對照表,第 8 篇才會接上 KV)+ CI 的 check:types 步驟。
新專案的最小可用 wrangler.jsonc:
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2026-07-24", "compatibility_flags": ["nodejs_compat"], // only if you need Node APIs "observability": { "enabled": true } // on by default for new Workers}常用指令:
| 指令 | 說明 |
|---|---|
wrangler dev | 本機 workerd |
wrangler dev --env staging | 指定環境 |
wrangler deploy --dry-run --outdir out | 不需帳號,驗證設定與 bundle |
wrangler types --env-interface CloudflareBindings | 生成型別 |
wrangler types --check | CI 驗證型別是否過期 |
wrangler secret put KEY | 寫入 production secret |
wrangler kv key put k v --binding B | 寫本機 KV(加 --remote 才是線上) |
wrangler tail | 即時 log(僅限除錯,見第 39 篇) |
① 環境不繼承 vars 與 bindings,而且只給 warning
前面詳述過。每個 env 都要完整重寫。deploy 會成功、Worker 會壞。
② 照抄別人的 compatibility_date
包括 Cloudflare 官方頁面上寫死的 2025-03-07。日期是你的 runtime 版本鎖,要自己決定。
③ 手動裝 @cloudflare/workers-types 然後和 wrangler types 打架
兩份 runtime 型別同時存在會產生大量重複宣告錯誤。Worker 專案只用 wrangler types。
④ 把 worker-configuration.d.ts commit 進去
它綁定 compat date 和 wrangler 版本,會製造無意義的 diff,而且容易和實際設定不同步。加進 .gitignore,改用 wrangler types --check 在 CI 把關。
⑤ 以為 wrangler kv/d1/r2 指令在打 production
v4 起預設本機。要打線上加 --remote。
⑥ 把 secret 寫進 vars
vars 會出現在 dashboard、會出現在生成的型別檔(連值都是 literal type)。secret 一律走 wrangler secret put 或 .dev.vars。
⑦ 沿用已移除的設定
v4 會直接報錯:wrangler publish(→ deploy)、wrangler generate、node_compat、usage_model、legacy_assets、getBindingsProxy()(→ getPlatformProxy())。已棄用但還能跑:Workers Sites [site](→ assets,第 7 篇)、Service Environments(→ Wrangler Environments)。
本篇要記住的三句話
Section titled “本篇要記住的三句話”- **
compatibility_date是 runtime 版本鎖,不是樣板欄位。**它決定你的 bundle 裡有沒有 polyfill、你的型別檔裡有沒有MessageChannel。 - **
wrangler types是唯一的型別來源,而且它會讀.dev.vars。**用--env-interface預先避開 Hono 衝突,用--check在 CI 把關。 - named environment 什麼都不繼承。
vars和 bindings 每個環境都要重寫,忘了只會拿到 warning。
- Wrangler configuration
- Compatibility dates 與 compatibility flags 全表
- Remote bindings GA
- Wrangler v3 → v4 遷移
- TypeScript 與
wrangler types
下一篇:03. 執行模型:fetch handler、ctx 與請求生命週期 —— 一次 invocation 從進來到被回收的完整過程,以及 ctx.waitUntil() 到底在延長什麼。