说明:本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。
这是一篇“把 API 放回运行时”的实战页。Dynamic Workers 不是把一段字符串直接执行掉:先部署一个可信的 Loader Worker;Loader 再决定动态代码、它可拿到的 binding、网络出口和资源上限。真正重要的接口只有三层:WorkerLoader、WorkerCode 与 WorkerStub / entrypoint。
Dynamic Workers 仍在快速演进,本文按 2026-08-07 的官方文档整理。它目前面向 Workers Paid;部署前应重新核对限额、价格与 beta 状态。
先记住对象关系#
flowchart LR U["用户请求"] --> H["可信 Loader Worker"] H --> A["认证、租户解析、代码版本选择"] A --> L["env.LOADER.load() 或 get()"] L --> C["WorkerCode:modules + bindings + 网络策略"] C --> S["WorkerStub"] S --> E["getEntrypoint()"] E --> D["动态 Worker 的 fetch / RPC 入口"] D --> B["仅被显式授予的 capability"] H -. "ctx.exports Service Binding" .-> B
不要把 get(id) 当作“拿到一个常驻进程”。它只是尽力复用已加载 isolate 的缓存:同一个 ID 的两次请求也可能进入不同 isolate,getCodeCallback 也可能在以后再次调用。动态代码必须把可靠状态放在 Durable Objects、D1、R2 等外部能力中。
1. 配置与最小调用#
先在宿主 Worker 配置一个 Loader binding:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "tenant-code-host",
"main": "src/index.ts",
"compatibility_date": "2026-08-07",
"worker_loaders": [
{ "binding": "LOADER" }
]
}随后,宿主在每次请求中创建或取得一个 WorkerStub,再调用它的默认 entrypoint:
export default {
async fetch(request, env) {
const worker = env.LOADER.get(
"greeting:v3:7f61e2",
async () => ({
compatibilityDate: "2026-08-07",
mainModule: "index.js",
modules: {
"index.js": [
"export default {",
" fetch() {",
" return new Response('hello from dynamic code');",
" }",
"};",
].join("\n"),
},
globalOutbound: null,
limits: { cpuMs: 50, subRequests: 10 },
}),
);
return worker.getEntrypoint().fetch(request);
},
};get() 本身同步返回 stub,不需要 await;若代码尚未装载,首次向 stub 发出的请求会等待装载完成。装载失败时,实际调用会抛错。
2. API 速查:应该在什么时机调用谁#
| 对象 | 接口 | 使用方式与边界 |
|---|---|---|
env.LOADER | load(code) | 每次调用创建一个新的 Dynamic Worker;适合一次性、每次代码都不同的 AI 工具调用。 |
env.LOADER | get(id, getCodeCallback) | 用不可变版本 ID 尽力复用代码/isolate;适合同一租户同一版本的小应用。 |
WorkerStub | getEntrypoint() | 取得默认模块 entrypoint;通常接着调用 fetch(request)。 |
WorkerStub | getEntrypoint(name) | 取得命名的 WorkerEntrypoint,用于明确暴露的 RPC API。 |
WorkerCode | modules、mainModule | 给出入口模块和已打包的依赖图;这不是运行时 TypeScript 编译器。 |
WorkerCode | env | 把结构化可克隆数据或 Service Binding 作为动态代码的 env。 |
WorkerCode | globalOutbound | 决定全局 fetch()/connect() 是继承、拒绝还是被网关接管。 |
WorkerCode | limits、tails | 为不可信代码设置 CPU/subrequest 上限,并投递日志和异常到 Tail Worker。 |
WorkerCode 还可携带 compatibility flags 和实验开关;应把它们也视为版本的一部分。只要代码、依赖、权限、bindings、compatibility 配置任一项变化,就必须换新的 get() ID,例如 tenant-42:editor:v17:content-hash。
3. load() 还是 get()?#
| 场景 | 选择 | ID 原则 |
|---|---|---|
| LLM 刚生成、只运行一次的工具调用 | load() | 不需要缓存身份。 |
| 一个租户的固定版本预览应用 | get() | 租户 + 版本 + 内容 hash。 |
| 同一代码但权限不同 | get() | 权限也进入 ID;不能共用旧 ID。 |
| 调试期间每次都改 bundle | get() | 每次构建产生新 hash。 |
错误做法是把租户 ID 单独作为 ID,却在 callback 中根据“当前最新版本”返回不同代码。恢复或重新装载时,缓存语义会变得不确定,也可能把旧会话悄悄切到新权限。
4. 不要把 KV、密钥或数据库直接交给动态代码#
最小权限的核心是:动态 Worker 得到的是业务 capability,不是原始 Cloudflare binding。下面的宿主 TenantStore 在可信 Worker 内运行,因此可进行鉴权、租户隔离与审计;动态代码只能调用它允许的 readProfile()。
import { WorkerEntrypoint } from "cloudflare:workers";
export class TenantStore extends WorkerEntrypoint {
async readProfile() {
const tenantId = this.ctx.props.tenantId;
return this.env.PROFILES.get(tenantId + ":profile");
}
}
export default {
async fetch(request, env, ctx) {
const tenantId = await authenticateAndGetTenantId(request);
const version = await getPublishedVersion(tenantId);
const worker = env.LOADER.get(
"tenant:" + tenantId + ":v:" + version,
async () => ({
compatibilityDate: "2026-08-07",
mainModule: "index.js",
modules: {
"index.js": await loadBundledCode(tenantId, version),
},
env: {
STORE: ctx.exports.TenantStore({
props: { tenantId },
}),
},
globalOutbound: null,
limits: { cpuMs: 50, subRequests: 10 },
}),
);
return worker.getEntrypoint().fetch(request);
},
};动态代码一侧只需了解自己被授予的接口:
export default {
async fetch(_request, env) {
const profile = await env.STORE.readProfile();
return Response.json({ profile });
},
};这样做有三个直接好处:
- 动态代码看不到
PROFILESbinding,也看不到宿主密钥。 - 宿主可以对每个方法做参数验证、配额、日志和对象级授权。
- 将来改变存储实现时,动态代码不需要知道 KV、D1 或 DO 的细节。
5. 网络是另一个 capability,不是默认便利功能#
如果省略 globalOutbound,Dynamic Worker 会继承 Loader 的网络能力,通常意味着可访问公共互联网。对于用户上传或 AI 生成的代码,默认值应是:
globalOutbound: null只有确实需要请求第三方时,才把全局出站指到宿主的网关 Service Binding。网关负责 allowlist、方法限制、请求大小、速率、审计以及在宿主侧注入密钥;动态代码只使用普通 fetch(),永远不接触 token。
export class OutboundGateway extends WorkerEntrypoint {
async fetch(request) {
const url = new URL(request.url);
const allowed = new Set(["api.example.com", "hooks.example.net"]);
if (!allowed.has(url.hostname)) {
return new Response("destination not allowed", { status: 403 });
}
const headers = new Headers(request.headers);
headers.set("authorization", "Bearer " + this.env.PARTNER_TOKEN);
return fetch(new Request(request, { headers }));
}
}
// WorkerCode 中:
// globalOutbound: ctx.exports.OutboundGateway({ props: { tenantId } })
6. 模块、入口、状态与可观测性#
modules可承载 JavaScript、CommonJS、Python、文本、数据和 JSON 模块;普通字符串源文件要使用正确的扩展名。- TypeScript 与 npm 依赖必须在交给 Loader 前完成转译和 bundle。不要期待生产请求在运行时安装依赖。
- Python 需要相应的
python_workerscompatibility flag,且启动特性与 JavaScript 不同,应单独压测。 limits可在WorkerCode或 entrypoint 调用处收紧;有效上限取更小值。遇到超过 CPU/subrequest 上限的异常时,按失败处理而不是无限重试。- 使用
tails将日志、异常和请求执行信息送到 Tail Worker;日志至少带 tenant ID、代码版本与请求 ID,但不要写入原始密钥或用户敏感数据。 - 动态代码若真正需要按实体持久化/串行协调,应考虑 Durable Object Facets 或宿主提供的受控 DO capability,而不是依赖 isolate 内存。
7. 上线前最短检查单#
-
get()的 ID 同时包含代码、依赖、权限和 compatibility 配置的版本信息。 - 不可信代码的
globalOutbound显式设为null,或明确指向审计网关。 - 动态代码得到的是小型 RPC capability,而不是原始 KV、R2、D1、DO 或 secret。
- 代码在部署前已 bundle;TypeScript、npm 依赖和 Python flag 都已验证。
- CPU、subrequest、日志、租户配额和异常处理有明确策略。
- 任何需要可靠状态的设计都已移到 DO / D1 / R2 等持久层。