跳到內容

工具鏈:Wrangler v4、`wrangler.jsonc` 與本機開發

查證日期
驗證環境wrangler@4.114.0·workerd@1.20260722.1

上一篇談的是「Workers 是什麼」。這一篇要建立你之後 41 篇都會用到的工作環境,並且釐清三件會反覆咬人的事:

  1. compatibility_date 不是樣板欄位。 它是 runtime 的版本鎖。同一份原始碼、只改這個日期,我實測到 bundle 大小差了 117 倍
  2. wrangler types 產生的型別會隨設定改變。 它不只是 @cloudflare/workers-types 的替代品,它會把你的 vars 變成 literal type、把 .dev.vars 的 secret 也算進去。
  3. 本機跑的是真的 workerd,但不是真的 production。 Wrangler v4 起大部分資料指令預設操作本機,而 v3 時代的教學指令複製過來會靜默地打錯目標。

目前的 CLI 是 Wrangler v4(本文使用 4.114.0)。沒有 v5;v3 的 bugfix 支援已於 2026 年 Q1 結束。

設定檔格式有 wrangler.tomlwrangler.jsonc 兩種。官方的說法很直接:

“Cloudflare recommends using wrangler.jsonc for 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 要驗證過再用。

這是整篇最重要的一節。

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):

Terminal window
$ wrangler deploy --dry-run --outdir out # compatibility_date: 2025-01-01
Total Upload: 25.30 KiB / gzip: 5.88 KiB
$ wrangler deploy --dry-run --outdir out # compatibility_date: 2026-07-24
Total Upload: 0.22 KiB / gzip: 0.17 KiB

25,903 bytes vs 221 bytes。 打開產出的 bundle 就知道為什麼:

2025-01-01 的版本裡塞滿了 polyfill:

../../node_modules/unenv/dist/runtime/_internal/utils.mjs
function createNotImplementedError(name) {
return new Error(`[unenv] ${name} is not implemented yet!`);
}

(bundle 裡出現 32 處 unenv 引用。)

2026-07-24 的版本則是:

src/index.ts
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-15enable_nodejs_http_modules
2025-09-15enable_nodejs_fs_moduleosprocess_v2
2026-01-29enable_nodejs_sqlitedgraminspector
2026-03-17child_processworker_threadsperf_hooksv8tty
2026-06-16remove_nodejs_compat_eol_v24throw_on_not_implemented_tls_options

回想第 1 篇的 script size 限制(Free 3MB / Paid 10MB,壓縮後)—— 一個過時的 compatibility_date 會讓你莫名其妙地更接近那道牆,而錯誤訊息不會告訴你原因出在日期上。

同一個專案,只改日期後重跑 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 的世界裡,navigatorMessageChannel 在型別層面根本不存在。這不是型別定義偷懶,是那個日期的 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。改用:

Terminal window
wrangler types

它產生 worker-configuration.d.ts,內容包含兩部分:你的 binding 型別 + 對應 compat date 的完整 runtime 型別。

官方的說法是:@cloudflare/workers-types 沒有被廢棄,但它現在的定位是「給函式庫和共用套件用」,Worker 專案本身應該用 wrangler types

拿這份設定:

{
"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;
}
}

四件值得注意的事:

  1. vars 拿到的是 literal type,不是 string APP_NAME: "ch02-toolchain"
  2. 跨環境的 var 會被 union 起來TIER: "staging" | "dev"。所以 if (env.TIER === "production") 會直接是編譯錯誤 —— 這很好,但如果你不知道會很困惑。
  3. .dev.vars 裡的 secret 也會被讀進來,型別是 string。這代表 .dev.vars 不只是本機注入,它同時是你的 secret 清單宣告。
  4. 標頭寫死了 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" } }
Terminal window
$ 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 前置步驟。

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”
Terminal window
$ 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 想不通資料去哪了。

模式指令資料在哪什麼時候用
全本機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 Mode
env.SETTINGS (0000000000000000000000000000eeee) KV Namespace local
env.APP_NAME ("ch02-toolchain") Environment Variable local
env.TIER ("staging") Environment Variable local
env.API_KEY ("(hidden)") Environment Variable local

⚠️ 舊名 experimental_remote 在 GA 時已改為 remote。另外 preview_id / preview_bucket_name / preview_database_id 這些欄位只是為了服務 legacy 的 --remote 而存在,新專案不需要。

本機.dev.vars務必加進 .gitignore):

API_KEY="local-dev-key-not-a-real-secret"

Production 用:

Terminal window
wrangler secret put API_KEY
wrangler secret bulk .prod.vars # 批次

Secret 一旦寫入就讀不回來,只能覆寫。第 41 篇會談到 Secrets Store(帳號層級共用 secret)。

多環境用 env.<name>,部署時 wrangler deploy --env staging注意這是 Wrangler Environments,不是已棄用的 Service Environments。

這是我認為第 2 篇最值得記住的一個坑。把 staging 寫成這樣:

{
"vars": { "APP_NAME": "ch02-toolchain", "TIER": "dev" },
"kv_namespaces": [{ "binding": "SETTINGS", "id": "...ffff" }],
"env": {
"staging": { "vars": { "TIER": "staging" } }
}
}

直覺上 staging 應該繼承 APP_NAMESETTINGS,只覆寫 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 Resource
env.TIER ("staging") Environment Variable

APP_NAMESETTINGS 整個消失了。 而且注意這是 WARNING 不是 ERROR —— wrangler deploy --env staging 會成功,然後你的 Worker 在 staging 上因為 env.SETTINGSundefined 而爆炸。

規則:每個 named environment 都必須完整重新宣告自己的 vars 和所有 bindings。 相對地,compatibility_datecompatibility_flagsmain 這類頂層設定會繼承的。

如果你受不了這種重複(我也受不了),第 26 篇會談 monorepo 裡用腳本生成設定的做法。


完整程式碼:examples/ch02-toolchain/

一支 Worker,同時用到 var、secret、KV binding 與兩個環境。

{
"$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。

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>;
Terminal window
npm install
echo 'API_KEY="local-dev-key-not-a-real-secret"' > .dev.vars
npm run cf-typegen
# Write to local KV — no login required, no --local flag needed
npx wrangler kv key put greeting "hello from local KV" --binding SETTINGS
npm run dev

預設環境:

Terminal window
$ curl -s localhost:8787/config
{"appName":"ch02-toolchain","tier":"dev","hasApiKey":true}
$ curl -s localhost:8787/settings
{"greeting":"hello from local KV"}

staging 環境:

Terminal window
$ 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,本機儲存也跟著分開。這正好證明了環境隔離在本機就是真的隔離,不是假裝的。

Terminal window
# 1. Types are in sync with config
npx 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 needed
npx wrangler deploy --dry-run --outdir out
ls -la out/

wrangler deploy --dry-run --outdir 是本系列會反覆用到的工具:不需要 Cloudflare 帳號就能驗證設定、看 bundle 大小、檢查有沒有意外打包進 polyfill。 examples repo 的 CI 就是靠它做第一層把關。


這一篇替 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.varsworker-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 --checkCI 驗證型別是否過期
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 generatenode_compatusage_modellegacy_assetsgetBindingsProxy()(→ getPlatformProxy())。已棄用但還能跑:Workers Sites [site](→ assets,第 7 篇)、Service Environments(→ Wrangler Environments)。


  1. **compatibility_date 是 runtime 版本鎖,不是樣板欄位。**它決定你的 bundle 裡有沒有 polyfill、你的型別檔裡有沒有 MessageChannel
  2. **wrangler types 是唯一的型別來源,而且它會讀 .dev.vars。**用 --env-interface 預先避開 Hono 衝突,用 --check 在 CI 把關。
  3. named environment 什麼都不繼承。vars 和 bindings 每個環境都要重寫,忘了只會拿到 warning。


下一篇03. 執行模型:fetch handler、ctx 與請求生命週期 —— 一次 invocation 從進來到被回收的完整過程,以及 ctx.waitUntil() 到底在延長什麼。