本文基于生产环境实践、Cloudflare 内部用法以及开发者社区常见问题,总结 Workers 最佳实践。
compatibility_date 决定 Worker 可使用哪些运行时特性与 bug 修复。新项目将其设为当天日期,可确保获得最新行为。现有项目定期更新它,无需改代码即可使用新 API 与修复。
{
"name": "my-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": ["nodejs_compat"],
}name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = [ "nodejs_compat" ]更多信息请参阅 兼容性日期。
nodejs_compat 兼容性标志让 Worker 可访问 node:crypto、node:buffer、node:stream 等 Node.js 内置模块。许多库依赖这些模块,启用该标志可避免运行时出现难以理解的导入错误。
{
"name": "my-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": ["nodejs_compat"],
}name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = [ "nodejs_compat" ]更多信息请参阅 Node.js 兼容性。
不要手写 Env 接口。运行 wrangler types 生成与 Wrangler 配置匹配的类型定义文件。这样可在编译期而非部署期发现配置与代码不一致的问题。
添加或重命名 binding 后,请重新运行 wrangler types。
npx wrangler typesyarn wrangler typespnpm wrangler types// ✅ Good: Env is generated by wrangler types and always matches your config
// Do not manually define Env — it drifts from your actual bindings
export default {
async fetch(request, env) {
// env.MY_KV, env.MY_BUCKET, etc. are all correctly typed
const value = await env.MY_KV.get("key");
return new Response(value);
},
};// ✅ Good: Env is generated by wrangler types and always matches your config
// Do not manually define Env — it drifts from your actual bindings
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// env.MY_KV, env.MY_BUCKET, etc. are all correctly typed
const value = await env.MY_KV.get("key");
return new Response(value);
},
} satisfies ExportedHandler<Env>;更多信息请参阅 wrangler types。
密钥(API 密钥、令牌、数据库凭据)绝不能出现在 Wrangler 配置或源代码中。使用 wrangler secret put 安全存储,并在运行时通过 env 访问。本地开发时使用 .env 文件(并确保已加入 .gitignore)。更多信息请参阅 环境变量。
{
"name": "my-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": ["nodejs_compat"],
// ✅ Good: non-secret configuration lives in version control
"vars": {
"API_BASE_URL": "https://api.example.com",
},
// 🔴 Bad: never put secrets here
// "API_KEY": "sk-live-abc123..."
}name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = [ "nodejs_compat" ]
[vars]
API_BASE_URL = "https://api.example.com"添加密钥时,运行以下命令并在提示时交互式输入:
npx wrangler secret put API_KEYyarn wrangler secret put API_KEYpnpm wrangler secret put API_KEY也可从其他工具或环境变量管道传入密钥:
# Pipe from another CLI tool
npx some-cli-tool --get-secret | npx wrangler secret put API_KEY
# Pipe from an environment variable or .env file
echo "$API_KEY" | npx wrangler secret put API_KEY更多信息请参阅 Secrets。
Wrangler 环境 可将同一份代码部署到生产、预发布和开发等独立 Worker。每个环境会创建名为 {name}-{env} 的独立 Worker(例如 my-api-production 与 my-api-staging)。
各环境相互独立。binding 与 vars 需按环境分别声明,不会继承。请参阅 不可继承的键。根 Worker(无环境后缀)是独立部署。若无意使用它,请勿在未指定 --env 的情况下部署。
{
"name": "my-api",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": ["nodejs_compat"],
// This binding only applies to the root Worker
"kv_namespaces": [{ "binding": "CACHE", "id": "dev-kv-id" }],
"env": {
// Production environment: deploys as "my-api-production"
"production": {
"kv_namespaces": [{ "binding": "CACHE", "id": "prod-kv-id" }],
"routes": [
{ "pattern": "api.example.com/*", "zone_name": "example.com" },
],
},
// Staging environment: deploys as "my-api-staging"
"staging": {
"kv_namespaces": [{ "binding": "CACHE", "id": "staging-kv-id" }],
"routes": [
{ "pattern": "api-staging.example.com/*", "zone_name": "example.com" },
],
},
},
}name = "my-api"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = [ "nodejs_compat" ]
[[kv_namespaces]]
binding = "CACHE"
id = "dev-kv-id"
[[env.production.kv_namespaces]]
binding = "CACHE"
id = "prod-kv-id"
[[env.production.routes]]
pattern = "api.example.com/*"
zone_name = "example.com"
[[env.staging.kv_namespaces]]
binding = "CACHE"
id = "staging-kv-id"
[[env.staging.routes]]
pattern = "api-staging.example.com/*"
zone_name = "example.com"使用此配置文件部署到 staging 时:
npx wrangler deploy --env stagingyarn wrangler deploy --env stagingpnpm wrangler deploy --env staging更多信息请参阅 环境。
Workers 支持两种路由机制,用途不同:
- 自定义域名:Worker 即源站。Cloudflare 会自动创建 DNS 记录与 SSL 证书。当 Worker 处理某主机名的全部流量时使用。
- 路由:Worker 在现有源站之前运行。添加路由前,该主机名须已有 Cloudflare 代理(橙色云)DNS 记录。
路由最常见错误是缺少 DNS 记录。没有代理 DNS 记录时,对该主机名的请求会返回 ERR_NAME_NOT_RESOLVED,无法到达 Worker。若没有真实源站,可添加指向 100:: 的代理 AAAA 记录作为占位。
{
"name": "my-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": ["nodejs_compat"],
// Option 1: Custom domain — Worker is the origin, DNS is managed automatically
"routes": [{ "pattern": "api.example.com", "custom_domain": true }],
// Option 2: Route — Worker runs in front of an existing origin
// Requires a proxied DNS record for shop.example.com
// "routes": [
// { "pattern": "shop.example.com/*", "zone_name": "example.com" }
// ]
}name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = [ "nodejs_compat" ]
[[routes]]
pattern = "api.example.com"
custom_domain = true更多信息请参阅 路由。
无论内存限制如何,流式传输大型请求与响应都是各语言中的最佳实践。它降低峰值内存占用并改善首字节时间。Workers 有 128 MB 内存限制,用 await response.text() 或 await request.arrayBuffer() 缓冲整个 body 会在大负载时导致 Worker 崩溃。
对于需要完整读取的请求体(JSON 负载、文件上传),读取前应限制最大尺寸,防止客户端发送你不愿处理的数据。
使用 TransformStream 在 Worker 中流式传输数据,从源管道到目标,无需全部驻留内存。
// 🔴 Bad: buffers the entire response body in memory
const badHandler = {
async fetch(request, env) {
const response = await fetch("https://api.example.com/large-dataset");
const text = await response.text();
return new Response(text);
},
};
// ✅ Good: stream the response body through without buffering
export default {
async fetch(request, env) {
const response = await fetch("https://api.example.com/large-dataset");
return new Response(response.body, response);
},
};// 🔴 Bad: buffers the entire response body in memory
const badHandler = {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await fetch("https://api.example.com/large-dataset");
const text = await response.text();
return new Response(text);
},
} satisfies ExportedHandler<Env>;
// ✅ Good: stream the response body through without buffering
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await fetch("https://api.example.com/large-dataset");
return new Response(response.body, response);
},
} satisfies ExportedHandler<Env>;需要拼接多个响应时(例如从多个上游 API 拉取数据),将各 body 依次写入同一 writable 流,避免在内存中缓冲任何响应。
export default {
async fetch(request, env) {
const urls = [
"https://api.example.com/part-1",
"https://api.example.com/part-2",
"https://api.example.com/part-3",
];
const { readable, writable } = new TransformStream();
// ✅ Good: pipe each response body sequentially without buffering
const pipeline = (async () => {
for (const url of urls) {
const response = await fetch(url);
if (response.body) {
// pipeTo with preventClose keeps the writable open for the next response
await response.body.pipeTo(writable, {
preventClose: true,
});
}
}
await writable.close();
})();
// Return the readable side immediately — data streams as it arrives
return new Response(readable, {
headers: { "Content-Type": "application/octet-stream" },
});
},
};export default {
async fetch(request: Request, env: Env): Promise<Response> {
const urls = [
"https://api.example.com/part-1",
"https://api.example.com/part-2",
"https://api.example.com/part-3",
];
const { readable, writable } = new TransformStream();
// ✅ Good: pipe each response body sequentially without buffering
const pipeline = (async () => {
for (const url of urls) {
const response = await fetch(url);
if (response.body) {
// pipeTo with preventClose keeps the writable open for the next response
await response.body.pipeTo(writable, {
preventClose: true,
});
}
}
await writable.close();
})();
// Return the readable side immediately — data streams as it arrives
return new Response(readable, {
headers: { "Content-Type": "application/octet-stream" },
});
},
} satisfies ExportedHandler<Env>;更多信息请参阅 Streams。
ctx.waitUntil() 可在响应已发送给客户端后继续执行工作,例如分析、缓存写入、日志或 webhook 通知。这样响应更快,后台任务仍能完成。
仅对不影响响应的工作使用 ctx.waitUntil()。若响应依赖该工作,应在返回响应前 await,或在工作完成时流式返回响应。仍在流式传输响应体的 Worker 无需 ctx.waitUntil() 也会保持活跃。
常见陷阱有两类:解构 ctx(会丢失 this 绑定并抛出 "Illegal invocation"),以及响应已发送或客户端断开后超出 30 秒 waitUntil() 时限。
// 🔴 Bad: destructuring ctx loses the `this` binding
const badHandler = {
async fetch(request, env, ctx) {
const { waitUntil } = ctx; // "Illegal invocation" at runtime
waitUntil(fetch("https://analytics.example.com/events"));
return new Response("OK");
},
};
// ✅ Good: send the response immediately, do background work after
export default {
async fetch(request, env, ctx) {
const data = await processRequest(request);
ctx.waitUntil(logToAnalytics(env, data));
ctx.waitUntil(updateCache(env, data));
return Response.json(data);
},
};
async function logToAnalytics(env, data) {
await fetch("https://analytics.example.com/events", {
method: "POST",
body: JSON.stringify(data),
});
}
async function updateCache(env, data) {
await env.CACHE.put("latest", JSON.stringify(data));
}// 🔴 Bad: destructuring ctx loses the `this` binding
const badHandler = {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const { waitUntil } = ctx; // "Illegal invocation" at runtime
waitUntil(fetch("https://analytics.example.com/events"));
return new Response("OK");
},
} satisfies ExportedHandler<Env>;
// ✅ Good: send the response immediately, do background work after
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const data = await processRequest(request);
ctx.waitUntil(logToAnalytics(env, data));
ctx.waitUntil(updateCache(env, data));
return Response.json(data);
},
} satisfies ExportedHandler<Env>;
async function logToAnalytics(env: Env, data: unknown): Promise<void> {
await fetch("https://analytics.example.com/events", {
method: "POST",
body: JSON.stringify(data),
});
}
async function updateCache(env: Env, data: unknown): Promise<void> {
await env.CACHE.put("latest", JSON.stringify(data));
}更多信息请参阅 Context。
R2、KV、D1、Queues、Workflows 等 Cloudflare 服务可作为 binding 使用。binding 是进程内直接引用,无需网络跳转、认证或额外延迟。在 Worker 内调用 REST API 会浪费时间并增加不必要的复杂度。
// 🔴 Bad: calling the REST API from a Worker
const badHandler = {
async fetch(request, env) {
const response = await fetch(
"https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/r2/buckets/BUCKET_NAME/objects/my-file",
{ headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` } },
);
return new Response(response.body);
},
};
// ✅ Good: use the binding directly — no network hop, no auth needed
export default {
async fetch(request, env) {
const object = await env.MY_BUCKET.get("my-file");
if (!object) {
return new Response("Not found", { status: 404 });
}
return new Response(object.body, {
headers: {
"Content-Type":
object.httpMetadata?.contentType ?? "application/octet-stream",
},
});
},
};// 🔴 Bad: calling the REST API from a Worker
const badHandler = {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await fetch(
"https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/r2/buckets/BUCKET_NAME/objects/my-file",
{ headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` } },
);
return new Response(response.body);
},
} satisfies ExportedHandler<Env>;
// ✅ Good: use the binding directly — no network hop, no auth needed
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const object = await env.MY_BUCKET.get("my-file");
if (!object) {
return new Response("Not found", { status: 404 });
}
return new Response(object.body, {
headers: {
"Content-Type":
object.httpMetadata?.contentType ?? "application/octet-stream",
},
});
},
} satisfies ExportedHandler<Env>;长时间运行、可重试或非紧急任务不应阻塞请求。使用 Queues 与 Workflows 将工作移出关键路径。二者用途不同:
使用 Queues 当 你需要解耦生产者与消费者。Queues 是消息代理:一个 Worker 发送消息,另一个 Worker 稍后处理。适合扇出(一个事件触发多个消费者)、缓冲与批处理(写入下游服务前聚合消息),以及简单单步后台任务(发邮件、触发 webhook、写日志)。Queues 提供至少一次投递,且每条消息可配置重试。
使用 Workflows 当 后台工作有多步且相互依赖。Workflows 是持久执行引擎:每步返回值会被持久化,某步失败时仅重试该步,而非整个任务。适合多步流程(扣款、创建发货、发送确认)、需暂停与恢复的长任务(通过 step.waitForEvent() 等待数小时或数天的外部事件或人工审批),以及后续步骤依赖先前结果的复杂条件逻辑。Workflows 可运行数小时、数天或数周。
两者结合 当高吞吐入口需要接入复杂处理时。例如 Queue 可缓冲传入订单,消费者为每个需多步履约的订单创建 Workflow 实例。
export default {
async fetch(request, env) {
const order = await request.json();
if (order.type === "simple") {
// ✅ Queue: single-step background job — send a message for async processing
await env.ORDER_QUEUE.send({
orderId: order.id,
action: "send-confirmation-email",
});
} else {
// ✅ Workflow: multi-step durable process — payment, fulfillment, notification
const instance = await env.FULFILLMENT_WORKFLOW.create({
params: { orderId: order.id },
});
}
return Response.json({ status: "accepted" }, { status: 202 });
},
};export default {
async fetch(request: Request, env: Env): Promise<Response> {
const order = await request.json<{ id: string; type: string }>();
if (order.type === "simple") {
// ✅ Queue: single-step background job — send a message for async processing
await env.ORDER_QUEUE.send({
orderId: order.id,
action: "send-confirmation-email",
});
} else {
// ✅ Workflow: multi-step durable process — payment, fulfillment, notification
const instance = await env.FULFILLMENT_WORKFLOW.create({
params: { orderId: order.id },
});
}
return Response.json({ status: "accepted" }, { status: 202 });
},
} satisfies ExportedHandler<Env>;一个 Worker 需要调用另一个时,使用 service binding,而非向公开 URL 发 HTTP 请求。service binding 零成本、绕过公网,并支持类型安全的 RPC。
import { WorkerEntrypoint } from "cloudflare:workers";
// The "auth" Worker exposes RPC methods
export class AuthService extends WorkerEntrypoint {
async verifyToken(token) {
// Token verification logic
return { userId: "user-123", valid: true };
}
}
// The "api" Worker calls the auth Worker via a service binding
export default {
async fetch(request, env) {
const token = request.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
// ✅ Good: call another Worker via service binding RPC — no network hop
const auth = await env.AUTH_SERVICE.verifyToken(token);
if (!auth.valid) {
return new Response("Invalid token", { status: 403 });
}
return Response.json({ userId: auth.userId });
},
};import { WorkerEntrypoint } from "cloudflare:workers";
// The "auth" Worker exposes RPC methods
export class AuthService extends WorkerEntrypoint {
async verifyToken(
token: string,
): Promise<{ userId: string; valid: boolean }> {
// Token verification logic
return { userId: "user-123", valid: true };
}
}
// The "api" Worker calls the auth Worker via a service binding
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const token = request.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
// ✅ Good: call another Worker via service binding RPC — no network hop
const auth = await env.AUTH_SERVICE.verifyToken(token);
if (!auth.valid) {
return new Response("Invalid token", { status: 403 });
}
return Response.json({ userId: auth.userId });
},
} satisfies ExportedHandler<Env>;从 Worker 连接远程 PostgreSQL 或 MySQL 时,始终使用 Hyperdrive。Hyperdrive 在数据库附近维护区域连接池,消除每次请求的 TCP 握手、TLS 协商与连接建立成本,并在可能时缓存查询结果。
每个请求创建新的 Client。Hyperdrive 管理底层连接池,因此创建客户端很快。数据库驱动需要 nodejs_compat。
{
"name": "my-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": ["nodejs_compat"],
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<YOUR_HYPERDRIVE_ID>" }],
}name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = [ "nodejs_compat" ]
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<YOUR_HYPERDRIVE_ID>"import { Client } from "pg";
export default {
async fetch(request, env) {
// ✅ Good: create a new client per request — Hyperdrive pools the underlying connection
const client = new Client({
connectionString: env.HYPERDRIVE.connectionString,
});
try {
await client.connect();
const result = await client.query("SELECT id, name FROM users LIMIT 10");
return Response.json(result.rows);
} catch (e) {
console.error(
JSON.stringify({ message: "database query failed", error: String(e) }),
);
return Response.json({ error: "Database error" }, { status: 500 });
}
},
};
// 🔴 Bad: connecting directly to a remote database without Hyperdrive
// Every request pays the full TCP + TLS + auth cost (often 300-500ms)
const badHandler = {
async fetch(request, env) {
const client = new Client({
connectionString: "postgres://user:[email protected]:5432/mydb",
});
await client.connect();
const result = await client.query("SELECT id, name FROM users LIMIT 10");
return Response.json(result.rows);
},
};import { Client } from "pg";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// ✅ Good: create a new client per request — Hyperdrive pools the underlying connection
const client = new Client({
connectionString: env.HYPERDRIVE.connectionString,
});
try {
await client.connect();
const result = await client.query("SELECT id, name FROM users LIMIT 10");
return Response.json(result.rows);
} catch (e) {
console.error(
JSON.stringify({ message: "database query failed", error: String(e) }),
);
return Response.json({ error: "Database error" }, { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
// 🔴 Bad: connecting directly to a remote database without Hyperdrive
// Every request pays the full TCP + TLS + auth cost (often 300-500ms)
const badHandler = {
async fetch(request: Request, env: Env): Promise<Response> {
const client = new Client({
connectionString: "postgres://user:[email protected]:5432/mydb",
});
await client.connect();
const result = await client.query("SELECT id, name FROM users LIMIT 10");
return Response.json(result.rows);
},
} satisfies ExportedHandler<Env>;更多信息请参阅 Hyperdrive。
普通 Worker 可将 HTTP 连接升级为 WebSocket,但缺少持久状态与休眠能力。若 isolate 被驱逐,连接会丢失,因为没有持久 actor 持有它。要可靠、长连接的 WebSocket,请使用 Durable Objects 与 Hibernation API。Durable Objects 在对象从内存驱逐后仍可保持 WebSocket 连接,并在消息到达时自动唤醒。
使用 this.ctx.acceptWebSocket() 而非 ws.accept() 以启用休眠。对 ping/pong 心跳使用 setWebSocketAutoResponse,无需唤醒对象。
import { DurableObject } from "cloudflare:workers";
// Parent Worker: upgrades HTTP to WebSocket and routes to a Durable Object
export default {
async fetch(request, env) {
if (request.headers.get("Upgrade") !== "websocket") {
return new Response("Expected WebSocket", { status: 426 });
}
const stub = env.CHAT_ROOM.getByName("default-room");
return stub.fetch(request);
},
};
// Durable Object: manages WebSocket connections with hibernation
export class ChatRoom extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
// Auto ping/pong without waking the object
this.ctx.setWebSocketAutoResponse(
new WebSocketRequestResponsePair("ping", "pong"),
);
}
async fetch(request) {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// ✅ Good: acceptWebSocket enables hibernation
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
// Called when a message arrives — the object wakes from hibernation if needed
async webSocketMessage(ws, message) {
for (const conn of this.ctx.getWebSockets()) {
conn.send(typeof message === "string" ? message : "binary");
}
}
async webSocketClose(ws, code, reason, wasClean) {
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(code, reason);
}
}import { DurableObject } from "cloudflare:workers";
// Parent Worker: upgrades HTTP to WebSocket and routes to a Durable Object
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.headers.get("Upgrade") !== "websocket") {
return new Response("Expected WebSocket", { status: 426 });
}
const stub = env.CHAT_ROOM.getByName("default-room");
return stub.fetch(request);
},
} satisfies ExportedHandler<Env>;
// Durable Object: manages WebSocket connections with hibernation
export class ChatRoom extends DurableObject {
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Auto ping/pong without waking the object
this.ctx.setWebSocketAutoResponse(
new WebSocketRequestResponsePair("ping", "pong"),
);
}
async fetch(request: Request): Promise<Response> {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
// ✅ Good: acceptWebSocket enables hibernation
this.ctx.acceptWebSocket(server);
return new Response(null, { status: 101, webSocket: client });
}
// Called when a message arrives — the object wakes from hibernation if needed
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
for (const conn of this.ctx.getWebSockets()) {
conn.send(typeof message === "string" ? message : "binary");
}
}
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean,
) {
// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
// auto-replies to Close frames. Calling close() is safe but no longer required.
ws.close(code, reason);
}
}更多信息请参阅 Durable Objects WebSocket 最佳实践。
Workers Static Assets 是在 Cloudflare 上部署静态站点、单页应用与全栈应用的推荐方式。若开始新项目,请使用 Workers 而非 Pages。Pages 仍可用,但新特性与优化集中在 Workers。
纯静态站点将 assets.directory 指向构建输出,无需 Worker 脚本。全栈应用添加 main 入口与 ASSETS binding,在 API 旁提供静态文件。
{
// Static site — no Worker script needed
"name": "my-static-site",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "./dist",
},
}name = "my-static-site"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = [ "nodejs_compat" ]
[assets]
directory = "./dist"更多信息请参阅 Workers Static Assets。
没有可观测性的生产 Worker 如同黑盒。部署到生产前请启用日志与追踪。出现间歇性错误时,需要已有采集的数据才能诊断。
在 Wrangler 配置中启用,并用 head_sampling_rate 控制流量与成本。采样率 1 表示全量采集;高流量 Worker 可调低。
使用结构化 JSON 与 console.log 记录日志,便于搜索与过滤。错误用 console.error,警告用 console.warn。它们在 Workers Observability 仪表板中会以对应严重级别显示。
{
"name": "my-worker",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-08-17",
"compatibility_flags": ["nodejs_compat"],
"observability": {
"enabled": true,
"logs": {
// Capture 100% of logs — lower this for high-traffic Workers
"head_sampling_rate": 1,
},
"traces": {
"enabled": true,
"head_sampling_rate": 0.01, // Sample 1% of traces
},
},
}name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-17"
compatibility_flags = [ "nodejs_compat" ]
[observability]
enabled = true
[observability.logs]
head_sampling_rate = 1
[observability.traces]
enabled = true
head_sampling_rate = 0.01export default {
async fetch(request, env) {
const url = new URL(request.url);
try {
// ✅ Good: structured JSON — searchable and filterable in the dashboard
console.log(
JSON.stringify({
message: "incoming request",
method: request.method,
path: url.pathname,
}),
);
const result = await env.MY_KV.get(url.pathname);
return new Response(result ?? "Not found", {
status: result ? 200 : 404,
});
} catch (e) {
// ✅ Good: console.error appears as "error" severity in Workers Observability
console.error(
JSON.stringify({
message: "request failed",
error: e instanceof Error ? e.message : String(e),
path: url.pathname,
}),
);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
};
// 🔴 Bad: unstructured string logs are hard to query
const badHandler = {
async fetch(request, env) {
const url = new URL(request.url);
console.log("Got a request to " + url.pathname);
return new Response("OK");
},
};export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
try {
// ✅ Good: structured JSON — searchable and filterable in the dashboard
console.log(
JSON.stringify({
message: "incoming request",
method: request.method,
path: url.pathname,
}),
);
const result = await env.MY_KV.get(url.pathname);
return new Response(result ?? "Not found", {
status: result ? 200 : 404,
});
} catch (e) {
// ✅ Good: console.error appears as "error" severity in Workers Observability
console.error(
JSON.stringify({
message: "request failed",
error: e instanceof Error ? e.message : String(e),
path: url.pathname,
}),
);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
// 🔴 Bad: unstructured string logs are hard to query
const badHandler = {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
console.log("Got a request to " + url.pathname);
return new Response("OK");
},
} satisfies ExportedHandler<Env>;更多信息请参阅 Workers Logs 与 Traces。
所有可观测性工具的说明请参阅 Workers Observability。
Workers 会在请求间复用 isolate。一个请求中设置的变量在下一请求中仍存在,导致跨请求数据泄漏、陈旧状态以及 "Cannot perform I/O on behalf of a different request" 错误。
通过函数参数传递状态,或存储在 env binding 上。切勿使用模块级变量。
// 🔴 Bad: global mutable state leaks between requests
let currentUser = null;
const badHandler = {
async fetch(request, env, ctx) {
// Storing request-scoped data globally means the next request sees stale data
currentUser = request.headers.get("X-User-Id");
const result = await handleRequest(currentUser, env);
return Response.json(result);
},
};
// ✅ Good: pass request-scoped data through function arguments
export default {
async fetch(request, env, ctx) {
const userId = request.headers.get("X-User-Id");
const result = await handleRequest(userId, env);
return Response.json(result);
},
};
async function handleRequest(userId, env) {
return { userId };
}// 🔴 Bad: global mutable state leaks between requests
let currentUser: string | null = null;
const badHandler = {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
// Storing request-scoped data globally means the next request sees stale data
currentUser = request.headers.get("X-User-Id");
const result = await handleRequest(currentUser, env);
return Response.json(result);
},
} satisfies ExportedHandler<Env>;
// ✅ Good: pass request-scoped data through function arguments
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const userId = request.headers.get("X-User-Id");
const result = await handleRequest(userId, env);
return Response.json(result);
},
} satisfies ExportedHandler<Env>;
async function handleRequest(userId: string | null, env: Env): Promise<object> {
return { userId };
}更多信息请参阅 Workers 错误。
未被 await、return 或传入 ctx.waitUntil() 的 Promise 是悬空 Promise。悬空 Promise 会导致静默 bug:结果丢失、错误被吞、工作未完成。Workers 运行时可能在悬空 Promise 完成前终止 isolate。
根据响应是否依赖该工作来选择。响应正确性依赖的工作用 await 或 return;可在响应发送后完成且在 waitUntil() 时限内结束的工作用 ctx.waitUntil()。
启用 no-floating-promises lint 规则以便在开发期捕获。若使用 ESLint,启用 @typescript-eslint/no-floating-promises ↗。若使用 oxlint,启用 typescript/no-floating-promises ↗。
# ESLint (typescript-eslint)
npx eslint --rule '{"@typescript-eslint/no-floating-promises": "error"}' src/
# oxlint
npx oxlint --deny typescript/no-floating-promises src/export default {
async fetch(request, env, ctx) {
const data = await request.json();
// 🔴 Bad: floating promise — result is dropped, errors are swallowed
fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify(data),
});
// ✅ Good: await if you need the result before responding
const response = await fetch("https://api.example.com/process", {
method: "POST",
body: JSON.stringify(data),
});
// ✅ Good: waitUntil if you do not need the result before responding
ctx.waitUntil(
fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify(data),
}),
);
return new Response("OK");
},
};export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const data = await request.json();
// 🔴 Bad: floating promise — result is dropped, errors are swallowed
fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify(data),
});
// ✅ Good: await if you need the result before responding
const response = await fetch("https://api.example.com/process", {
method: "POST",
body: JSON.stringify(data),
});
// ✅ Good: waitUntil if you do not need the result before responding
ctx.waitUntil(
fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify(data),
}),
);
return new Response("OK");
},
} satisfies ExportedHandler<Env>;Workers 运行时提供 Web Crypto API 用于加密操作。唯一标识用 crypto.randomUUID(),随机字节用 crypto.getRandomValues()。任何与安全相关的场景都不要用 Math.random(),它不具备密码学安全性。
启用 nodejs_compat 时,Node.js node:crypto 也完全支持,可按你或库的偏好选择 API。
export default {
async fetch(request, env) {
// 🔴 Bad: Math.random() is predictable and not suitable for security
const badToken = Math.random().toString(36).substring(2);
// ✅ Good: cryptographically secure random UUID
const sessionId = crypto.randomUUID();
// ✅ Good: cryptographically secure random bytes for tokens
const tokenBytes = new Uint8Array(32);
crypto.getRandomValues(tokenBytes);
const token = Array.from(tokenBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return Response.json({ sessionId, token });
},
};export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 🔴 Bad: Math.random() is predictable and not suitable for security
const badToken = Math.random().toString(36).substring(2);
// ✅ Good: cryptographically secure random UUID
const sessionId = crypto.randomUUID();
// ✅ Good: cryptographically secure random bytes for tokens
const tokenBytes = new Uint8Array(32);
crypto.getRandomValues(tokenBytes);
const token = Array.from(tokenBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return Response.json({ sessionId, token });
},
} satisfies ExportedHandler<Env>;比较密钥值(API 密钥、令牌、HMAC 签名)时,使用 crypto.subtle.timingSafeEqual() 防止时序侧信道攻击。不要在长度不匹配时短路返回。先将两个值编码为固定长度哈希再比较。
async function verifyToken(provided, expected) {
const encoder = new TextEncoder();
// ✅ Good: hash both values to a fixed size, then compare in constant time
// This avoids leaking the length of the expected value
const [providedHash, expectedHash] = await Promise.all([
crypto.subtle.digest("SHA-256", encoder.encode(provided)),
crypto.subtle.digest("SHA-256", encoder.encode(expected)),
]);
return crypto.subtle.timingSafeEqual(providedHash, expectedHash);
}
// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided, expected) {
return provided === expected;
}async function verifyToken(
provided: string,
expected: string,
): Promise<boolean> {
const encoder = new TextEncoder();
// ✅ Good: hash both values to a fixed size, then compare in constant time
// This avoids leaking the length of the expected value
const [providedHash, expectedHash] = await Promise.all([
crypto.subtle.digest("SHA-256", encoder.encode(provided)),
crypto.subtle.digest("SHA-256", encoder.encode(expected)),
]);
return crypto.subtle.timingSafeEqual(providedHash, expectedHash);
}
// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided: string, expected: string): boolean {
return provided === expected;
}passThroughOnException() 是 fail-open 机制:Worker 抛出未处理异常时将请求转发到源站。从源站迁移时可能有用,但会隐藏 bug 并增加调试难度。请改用显式 try...catch 与结构化错误响应。
// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
async fetch(request, env, ctx) {
ctx.passThroughOnException();
const result = await handleRequest(request, env);
return Response.json(result);
},
};
// ✅ Good: explicit error handling with structured responses
export default {
async fetch(request, env, ctx) {
try {
const result = await handleRequest(request, env);
return Response.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(
JSON.stringify({
message: "unhandled error",
error: message,
path: new URL(request.url).pathname,
}),
);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
};
async function handleRequest(request, env) {
return { status: "ok" };
}// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
ctx.passThroughOnException();
const result = await handleRequest(request, env);
return Response.json(result);
},
} satisfies ExportedHandler<Env>;
// ✅ Good: explicit error handling with structured responses
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
try {
const result = await handleRequest(request, env);
return Response.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(
JSON.stringify({
message: "unhandled error",
error: message,
path: new URL(request.url).pathname,
}),
);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
async function handleRequest(request: Request, env: Env): Promise<object> {
return { status: "ok" };
}@cloudflare/vitest-pool-workers 在 Workers 运行时内运行测试,测试期间可使用真实 binding(KV、R2、D1、Durable Objects)。这能发现基于 Node.js 的测试遗漏的问题,例如不支持的 API 或缺少兼容性标志。
已知陷阱:Vitest pool 会自动注入 nodejs_compat,因此即使 Wrangler 配置没有该标志,测试也可能通过。若代码依赖 Node.js 内置模块,请确认 wrangler.jsonc 包含 nodejs_compat。
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";
describe("KV operations", () => {
it("should store and retrieve a value", async () => {
await env.MY_KV.put("key", "value");
const result = await env.MY_KV.get("key");
expect(result).toBe("value");
});
it("should return null for missing keys", async () => {
const result = await env.MY_KV.get("nonexistent");
// ✅ Good: test the null case explicitly
expect(result).toBeNull();
});
});import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";
describe("KV operations", () => {
it("should store and retrieve a value", async () => {
await env.MY_KV.put("key", "value");
const result = await env.MY_KV.get("key");
expect(result).toBe("value");
});
it("should return null for missing keys", async () => {
const result = await env.MY_KV.get("nonexistent");
// ✅ Good: test the null case explicitly
expect(result).toBeNull();
});
});更多信息请参阅 使用 Vitest 测试。
- Durable Objects 规则:有状态、协调型应用的最佳实践。
- Workflows 规则:持久、多步 Workflows 的最佳实践。
- 平台限制:CPU 时间、内存、子请求等限制。
- Workers 错误:错误代码与调试指南。