跳转到内容
搜索文档

Workers 最佳实践

最后更新 查看 MarkdownAgent 设置

本文基于生产环境实践、Cloudflare 内部用法以及开发者社区常见问题,总结 Workers 最佳实践。

配置

保持 compatibility date 为最新

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

nodejs_compat 兼容性标志让 Worker 可访问 node:cryptonode:buffernode: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 兼容性

使用 wrangler types 生成 binding 类型

不要手写 Env 接口。运行 wrangler types 生成与 Wrangler 配置匹配的类型定义文件。这样可在编译期而非部署期发现配置与代码不一致的问题。

添加或重命名 binding 后,请重新运行 wrangler types

npx wrangler types
src/index.jsjs
// ✅ 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);
	},
};
src/index.tsts
// ✅ 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

使用 wrangler secret 存储密钥,不要写入源码

密钥(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_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-productionmy-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 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 中流式传输数据,从源管道到目标,无需全部驻留内存。

src/index.jsjs
// 🔴 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);
	},
};
src/index.tsts
// 🔴 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 流,避免在内存中缓冲任何响应。

src/concat.jsjs
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" },
		});
	},
};
src/concat.tsts
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

响应发送后用 waitUntil 处理后续工作

ctx.waitUntil() 可在响应已发送给客户端后继续执行工作,例如分析、缓存写入、日志或 webhook 通知。这样响应更快,后台任务仍能完成。

仅对不影响响应的工作使用 ctx.waitUntil()。若响应依赖该工作,应在返回响应前 await,或在工作完成时流式返回响应。仍在流式传输响应体的 Worker 无需 ctx.waitUntil() 也会保持活跃。

常见陷阱有两类:解构 ctx(会丢失 this 绑定并抛出 "Illegal invocation"),以及响应已发送或客户端断开后超出 30 秒 waitUntil() 时限。

src/index.jsjs
// 🔴 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));
}
src/index.tsts
// 🔴 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

架构

对 Cloudflare 服务使用 binding,而非 REST API

R2、KV、D1、Queues、Workflows 等 Cloudflare 服务可作为 binding 使用。binding 是进程内直接引用,无需网络跳转、认证或额外延迟。在 Worker 内调用 REST API 会浪费时间并增加不必要的复杂度。

src/index.jsjs
// 🔴 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",
			},
		});
	},
};
src/index.tsts
// 🔴 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 处理异步与后台工作

长时间运行、可重试或非紧急任务不应阻塞请求。使用 QueuesWorkflows 将工作移出关键路径。二者用途不同:

使用 Queues 当 你需要解耦生产者与消费者。Queues 是消息代理:一个 Worker 发送消息,另一个 Worker 稍后处理。适合扇出(一个事件触发多个消费者)、缓冲与批处理(写入下游服务前聚合消息),以及简单单步后台任务(发邮件、触发 webhook、写日志)。Queues 提供至少一次投递,且每条消息可配置重试。

使用 Workflows 当 后台工作有多步且相互依赖。Workflows 是持久执行引擎:每步返回值会被持久化,某步失败时仅重试该步,而非整个任务。适合多步流程(扣款、创建发货、发送确认)、需暂停与恢复的长任务(通过 step.waitForEvent() 等待数小时或数天的外部事件或人工审批),以及后续步骤依赖先前结果的复杂条件逻辑。Workflows 可运行数小时、数天或数周。

两者结合 当高吞吐入口需要接入复杂处理时。例如 Queue 可缓冲传入订单,消费者为每个需多步履约的订单创建 Workflow 实例。

src/index.jsjs
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 });
	},
};
src/index.tsts
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>;

更多信息请参阅 QueuesWorkflows

用 service binding 实现 Worker 间通信

一个 Worker 需要调用另一个时,使用 service binding,而非向公开 URL 发 HTTP 请求。service binding 零成本、绕过公网,并支持类型安全的 RPC。

src/index.jsjs
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 });
	},
};
src/index.tsts
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>;

用 Hyperdrive 连接外部数据库

从 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>"
src/index.jsjs
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);
	},
};
src/index.tsts
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

用 Durable Objects 处理 WebSocket

普通 Worker 可将 HTTP 连接升级为 WebSocket,但缺少持久状态与休眠能力。若 isolate 被驱逐,连接会丢失,因为没有持久 actor 持有它。要可靠、长连接的 WebSocket,请使用 Durable ObjectsHibernation API。Durable Objects 在对象从内存驱逐后仍可保持 WebSocket 连接,并在消息到达时自动唤醒。

使用 this.ctx.acceptWebSocket() 而非 ws.accept() 以启用休眠。对 ping/pong 心跳使用 setWebSocketAutoResponse,无需唤醒对象。

src/index.jsjs
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);
	}
}
src/index.tsts
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

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

可观测性

启用 Workers Logs 与 Traces

没有可观测性的生产 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.01
src/index.jsjs
export 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");
	},
};
src/index.tsts
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 LogsTraces

所有可观测性工具的说明请参阅 Workers Observability

代码模式

不要在全局作用域存储请求级状态

Workers 会在请求间复用 isolate。一个请求中设置的变量在下一请求中仍存在,导致跨请求数据泄漏、陈旧状态以及 "Cannot perform I/O on behalf of a different request" 错误。

通过函数参数传递状态,或存储在 env binding 上。切勿使用模块级变量。

src/index.jsjs
// 🔴 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 };
}
src/index.tsts
// 🔴 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 或 waitUntil 你的 Promise

未被 awaitreturn 或传入 ctx.waitUntil()Promise 是悬空 Promise。悬空 Promise 会导致静默 bug:结果丢失、错误被吞、工作未完成。Workers 运行时可能在悬空 Promise 完成前终止 isolate。

根据响应是否依赖该工作来选择。响应正确性依赖的工作用 awaitreturn;可在响应发送后完成且在 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/
src/index.jsjs
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");
	},
};
src/index.tsts
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>;

安全

用 Web Crypto 生成安全令牌

Workers 运行时提供 Web Crypto API 用于加密操作。唯一标识用 crypto.randomUUID(),随机字节用 crypto.getRandomValues()。任何与安全相关的场景都不要用 Math.random(),它不具备密码学安全性。

启用 nodejs_compat 时,Node.js node:crypto 也完全支持,可按你或库的偏好选择 API。

src/index.jsjs
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 });
	},
};
src/index.tsts
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() 防止时序侧信道攻击。不要在长度不匹配时短路返回。先将两个值编码为固定长度哈希再比较。

src/verify.jsjs
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;
}
src/verify.tsts
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 当作错误处理

passThroughOnException() 是 fail-open 机制:Worker 抛出未处理异常时将请求转发到源站。从源站迁移时可能有用,但会隐藏 bug 并增加调试难度。请改用显式 try...catch 与结构化错误响应。

src/index.jsjs
// 🔴 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" };
}
src/index.tsts
// 🔴 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 测试

@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

test/index.test.jsjs
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();
	});
});
test/index.test.tsts
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 测试

相关资源

这篇文档对您有帮助吗?