在 Cloudflare Workers 中运行时打包与动态执行 Worker:Dynamic Workers Playground 全解析
2026/9/18 3:36:00 网站建设 项目流程

在 Cloudflare Workers 中运行时打包与动态执行 Worker:Dynamic Workers Playground 全解析

【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents

导读

Dynamic Workers Playground 是当前仓库(Cloudflare Agents 示例集)中一个极具代表性的示例工程:它把@cloudflare/worker-bundler的运行时打包能力Dynamic Worker Loaders(worker_loaders绑定)的动态加载能力组合在一起,实现在一个 Worker 内部接收用户提交的源码、现场完成依赖解析与打包、再以动态 Worker 的形式执行并实时回流日志与耗时。读完本文,你将掌握运行时createWorker()的核心参数、worker_loaders绑定的配置方式、Tail Worker + Durable Object 的日志管道设计,以及一套可复用的"源码 → 打包 → 加载 → 执行 → 观测"完整链路。

一、示例概览:这个 Playground 做了什么

Dynamic Workers Playground 位于仓库的 examples/dynamic-workers-playground 目录。它的核心定位在 README 中写得很清楚:"Write, bundle, and run Cloudflare Worker code at runtime"—— 在运行时编写、打包并执行 Cloudflare Worker 代码。换言之,它的宿主 Worker 不是一份写死的业务代码,而是一个"代码执行平台",用户可以在浏览器里直接编辑 Worker 源码,点击Run Worker后由服务端现场完成打包与执行。

从源码结构看,整个示例分为两层:

  • 服务端(Server-side):src/server.ts 承载运行时打包与动态执行;src/logging.ts 承载日志采集管道。
  • 客户端(Client-side):src/client.tsx 提供带文件标签页的编辑器、示例加载、GitHub 导入与实时结果面板。

它演示的能力清单如下(对应 README 的 "What it demonstrates"):

  • 运行时打包:使用@cloudflare/worker-bundler在 Worker 内部解析 npm 依赖并打包源码;
  • 动态执行:通过worker_loaders绑定加载动态 Worker,并在源码未变化时自动命中缓存;
  • 日志采集管道:一个 Tail Worker(DynamicWorkerTail)把动态 Worker 的console.*输出转发到 Durable Object(LogSession),再实时流回调用方;
  • 执行计时:粒度化的 build / load / run 分段耗时,并区分冷启动(cold)与热启动(warm);
  • 客户端能力:带 Tab 键缩进的标签页式文件编辑器、加载内置示例或导入任意 GitHub 仓库、可透传的 bundle / minify 开关、实时输出(响应体、控制台日志、耗时与打包信息)。

二、快速开始

按 README 的指引,从仓库根目录安装依赖,再进入示例目录启动开发服务器:

npm install # 在仓库根目录执行 npm start # 在 examples/dynamic-workers-playground 目录执行

npm start实际对应 package.json 中的脚本vite dev。示例同时提供了部署脚本:

"scripts": { "start": "vite dev", "deploy": "vite build && wrangler deploy", "types": "wrangler types env.d.ts --include-runtime false" }

本地开发时,vite.config.ts 通过@cloudflare/vite-plugin把 Vite 开发服务器与本地 Workers 运行时打通:

import { cloudflare } from "@cloudflare/vite-plugin"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [react(), cloudflare(), tailwindcss()] });

客户端界面(client.tsx)左侧是源码面板(文件标签页 + 编辑器 + Run Worker / Format 按钮 + Bundle / Minify 开关),右侧是输出面板(响应体、Console 日志、Timing 四格耗时、Bundle Info 模块清单),并带有运行状态指示点与明暗主题切换。

三、核心工作流:从点击 Run Worker 到响应返回

README 给出了整条链路的骨架代码,这也是理解整个示例的钥匙。当用户点击Run Worker时,宿主 Worker 收到源码文件,调用@cloudflare/worker-bundlercreateWorker()在运行时完成打包:

const { mainModule, modules, wranglerConfig, warnings } = await createWorker({ files: normalizedFiles, bundle: options?.bundle ?? true, minify: options?.minify ?? false }); const worker = env.LOADER.get(workerId, async () => ({ mainModule, modules, tails: [contextExports.DynamicWorkerTail({ props: { workerId } })] })); const response = await worker.getEntrypoint().fetch(request);

这一段对应 README 的 "How it works",其中包含三个关键概念:

  1. createWorker()打包期操作:它接收files(路径 → 内容)映射,解析依赖并产出mainModule(入口模块路径)、modules(模块集合)、wranglerConfig(打包时解析出的配置)与warnings
  2. env.LOADER.get(workerId, factory)加载期操作:LOADERworker_loaders绑定,第二个参数是惰性工厂函数,只有当该 workerId 对应的动态 Worker 尚未被缓存时才会执行,这正是"源码未变化时自动命中缓存"的原理所在;
  3. worker.getEntrypoint().fetch(request)执行期操作:拿到动态 Worker 的入口点后直接发起 fetch 调用。

下面我们结合 src/server.ts 的完整实现逐段拆解。

3.1 路由与请求校验

宿主 Worker 的fetch处理两个 POST 接口(server.ts):

  • /api/github:交给handleGitHubImport导入 GitHub 仓库文件;
  • /api/run:核心执行接口,接收RunRequestBody
interface RunRequestBody { files: Record<string, string>; // 源码文件:路径 -> 内容 version: number; // 客户端维护的版本号 pathname?: string; // 可选,动态 Worker 内请求的路径 options?: { bundle?: boolean; // 默认 true minify?: boolean; // 默认 false }; }

接口会先校验files非空(否则返回 400"At least one source file is required."),然后进入normalizeFiles

3.2 源码规范化:自动补全 package.json

normalizeFiles(server.ts)会剔除空路径文件,并在缺少package.json时自动生成一份,用于告诉createWorker入口文件在哪里:

if (!normalized["package.json"]) { const entryPoint = normalized["src/index.ts"] || normalized["src/index.js"] ? Object.keys(normalized).find( (file) => file === "src/index.ts" || file === "src/index.js" ) : Object.keys(normalized).find( (file) => file.endsWith(".ts") || file.endsWith(".js") ); normalized["package.json"] = JSON.stringify( { name: "dynamic-workers-playground-worker", main: entryPoint ?? "src/index.ts" }, null, 2 ); }

可见入口解析的优先级是:src/index.ts/src/index.js优先,否则回退到第一个.ts.js文件,最后兜底src/index.ts。这正是createWorkerentryPoint未显式指定时,package.jsonmain字段参与入口判定的实际落地(参见packages/worker-bundler/src/types.tsCreateWorkerOptions.entryPoint的注释:入口可按wrangler.tomlmainpackage.json→ 默认路径顺序推断)。

3.3 稳定的 Worker ID:内容寻址缓存

动态执行要命中缓存,前提是"同样的源码对应同样的 ID"。createWorkerId(server.ts)把排序后的文件列表连同bundle/minify选项序列化,再做SHA-256 摘要并截取前 16 位十六进制

const payload = JSON.stringify({ files: sortedFiles, bundle: options?.bundle ?? true, minify: options?.minify ?? false }); const digest = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(payload) ); // ... return `dynamic-workers-playground-worker-${hash}`;

这里对files先按路径排序,保证同样的内容集合无论以何种顺序提交都得到相同的 workerId。文件名前缀dynamic-workers-playground-worker-也避免了与其他示例的加载器命名空间冲突。从源码结构可以推断,这个 ID 既是LOADER.get的缓存键,也是LogSession.getByName日志会话的名字(见下文),让"缓存命中"与"日志归位"共用同一把钥匙。

3.4 打包与加载:createWorker + LOADER.get

核心执行逻辑(server.ts):

const worker = env.LOADER.get(workerId, async () => { const buildStart = Date.now(); const { mainModule, modules, wranglerConfig, warnings } = await createWorker({ files: normalizedFiles, bundle: options?.bundle ?? true, minify: options?.minify ?? false }); state.buildTime = Date.now() - buildStart; state.bundleInfo = { mainModule, modules: Object.keys(modules), warnings: warnings ?? [] }; return { mainModule, modules: modules as Record<string, string>, compatibilityDate: wranglerConfig?.compatibilityDate ?? "2026-01-01", compatibilityFlags: wranglerConfig?.compatibilityFlags ?? [], env: { API_KEY: "sk-example-key-12345", DEBUG: "true", WORKER_ID: workerId }, globalOutbound: null, tails: [ contextExports.DynamicWorkerTail({ props: { workerId } }) ] }; });

几个值得注意的实现细节:

  • createWorker返回值mainModule(入口路径)、modules(内容映射)、wranglerConfig(打包过程中解析出的compatibilityDate/compatibilityFlags)、warnings
  • 动态 Worker 的配置注入compatibilityDatecompatibilityFlags从打包产物的wranglerConfig继承,缺失时回退到"2026-01-01"与空数组;env展示了如何给动态 Worker 注入环境变量(示例注入API_KEYDEBUGWORKER_ID);globalOutbound置为null
  • tails挂载:为动态 Worker 挂上一个 Tail Worker 实例,props携带workerId,让日志管道知道日志归属哪个动态 Worker。
  • state捕获:由于LOADER.get的工厂函数是惰性的(仅首次构建时执行),示例用闭包把buildTimebundleInfo写入state对象;当缓存命中时工厂不再执行,state保持初值(buildTime: 0bundleInfo: null),executeWorker里便用(cached)占位符标记"未重新打包",从而区分本次是构建还是缓存命中。

关于worker_loaders绑定本身,它需要在 wrangler.jsonc 中显式声明:

{ "name": "dynamic-workers-playground", "main": "src/server.ts", "compatibility_date": "2026-06-11", "compatibility_flags": ["nodejs_compat"], "worker_loaders": [{ "binding": "LOADER" }] }

绑定名LOADER在运行时通过env.LOADER.get(id, factory)使用,其类型由wrangler types生成到 env.d.ts:

interface __BaseEnv_Env { LOADER: WorkerLoader; LogSession: DurableObjectNamespace<import("./src/server").LogSession>; }

3.5 执行与响应:冷/热启动计时与日志等待

executeWorker(server.ts)负责真正执行动态 Worker,并按阶段计时:

const entrypoint = worker.getEntrypoint() as Fetcher & { __warmup__?: () => Promise<void>; }; const loadStart = Date.now(); try { await entrypoint.__warmup__?.(); } catch { // Warmup intentionally calls a method that does not exist so the worker cold-starts. } const loadTime = Date.now() - loadStart;

这里有个巧妙设计:代码主动调用一个不存在的方法__warmup__并吞掉异常。从注释可以确认意图——当动态 Worker 是冷启动时,getEntrypoint()需要实例化运行时并解析模块,这个"故意调错方法"的过程会触发完整的冷启动路径,从而测得真实的加载耗时;而当动态 Worker 已被缓存(热启动)时,该调用接近零开销。这正好呼应 README 中"cold vs. warm start detection"的说明,也解释了客户端在Timing (cold/warm)标题里用loadTime > 0判定冷热的原因(见 client.tsx)。

接着,宿主通过runtimeExports.LogSession.getByName(workerId)拿到日志会话的 RPC stub,调用waitForLogs()建立一个等待器,然后才真正执行:

const logSessionStub = runtimeExports.LogSession.getByName(workerId); const logWaiter = await logSessionStub.waitForLogs(); const runStart = Date.now(); const request = new Request( `https://example.com${pathname.startsWith("/") ? pathname : `/${pathname}`}` );

值得注意的是,动态 Worker 收到的请求 URL 被规范化到https://example.com/<pathname>域名下(默认/),避免外部主机名干扰;随后调用entrypoint.fetch(request)执行并捕获异常。对 5xx 响应、运行时异常分别记录workerError,最终统一返回 JSON:

return Response.json({ bundleInfo: bundleInfo ?? { mainModule: "(cached)", modules: [], warnings: [] }, response: { status: workerResponse.status, headers, body: responseBody }, workerError, logs, timing: { buildTime, loadTime, runTime, totalTime: buildTime + loadTime + runTime } });

这里logs来自await logWaiter.getLogs(1000)—— 最多等待 1000ms 收集动态 Worker 执行期间的日志,超时则返回已收集的部分。响应体、响应头、状态码、日志与三段式耗时一次性回传给客户端。

四、@cloudflare/worker-bundlercreateWorkerAPI 详解

Playground 只用了createWorker的一小部分选项。该库类型定义位于 packages/worker-bundler/src/types.ts,其入口导出见 packages/worker-bundler/src/index.ts(createWorker在调用时还会打印实验性 API 提示)。完整参数如下,可作为自行集成时的参考:

参数类型默认值说明
filesFiles \| FileSystem必填输入文件,键为相对项目根目录的路径,值为文件内容
entryPointstring自动推断入口文件路径;未指定时按wrangler.tomlmainpackage.json→ 默认路径(如src/index.ts)依次推断
bundlebooleantrue是否把所有依赖打包进单一产物
externalsstring[][]不参与打包的外部模块;注意cloudflare:*模块始终被视为外部
targetstring'es2022'目标运行环境
minifybooleanfalse是否压缩产物
sourcemapbooleanfalse是否生成内联 source map(仅bundle: true时生效),便于调试与错误堆栈定位
registrystring'https://registry.npmjs.org'拉取 npm 包的 registry 地址
jsx'transform' \| 'preserve' \| 'automatic'传给 esbuild 的 JSX 转换模式;automatic启用新 JSX runtime(无需手动 import React);仅bundle: true生效
jsxImportSourcestringjsx: 'automatic'时 JSX runtime 的导入来源,如"react""preact""@emotion/react"
defineRecord<string, string>打包期常量替换,如{ "process.env.NODE_ENV": '"production"' };仅bundle: true生效
加载器覆盖types.ts按扩展名(含前导点,如".svg")覆盖 loader;.ts/.tsx/.js/.jsx/.json/.css的内置处理除非被覆盖否则保留

types.ts的注释可以确认:在bundle: false(仅转换不打包)模式下,sourcemapjsxdefine等选项不生效,jsxdefine还会被追加到返回结果的warnings数组中提示调用方。这与 Playground 把warnings原样展示在 Bundle Info 面板中的行为互相印证(client.tsx)。

五、日志采集管道:Tail Worker + Durable Object

README 强调的第三项核心能力是:"Log capture pipeline — a Tail Worker (DynamicWorkerTail) forwardsconsole.*output from dynamically loaded workers to a Durable Object (LogSession), streamed back to the caller in real time"。

实现位于 src/logging.ts,包含三个角色:

5.1 DynamicWorkerTail:Tail Worker 事件转换

DynamicWorkerTail继承WorkerEntrypoint并重写tail(events: TraceItem[])(logging.ts)。props.workerIdLOADER.get工厂里创建实例时传入。它把每条 Trace 事件转换成结构化事件并转发:

export class DynamicWorkerTail extends WorkerEntrypoint< never, DynamicWorkerTailProps > { override async tail(events: TraceItem[]) { const logSessionStub = exports.LogSession.getByName( this.ctx.props.workerId ); for (const event of events) { const structuredEvents: StructuredDynamicWorkerEvent[] = []; const requestSummary = toRequestSummary(event, this.ctx.props.workerId); if (requestSummary) structuredEvents.push(requestSummary); structuredEvents.push(...toLogEvents(event, this.ctx.props.workerId)); structuredEvents.push(...toExceptionEvents(event, this.ctx.props.workerId)); // ... await logSessionStub.addLogs(toRealtimeLogEntries(structuredEvents)); } } }

转换函数分工明确:

  • toRequestSummary:利用TraceItemFetchEventInfo提取请求方法、URL path、响应状态码与 outcome,生成形如GET / -> 200 (ok)的请求摘要(event.event上是否携带request字段用类型守卫isFetchTraceEvent判断);
  • toLogEvents:把event.logs中的每条TraceLog变成{ level, message, timestamp },消息经由normalizeLogMessage统一为字符串(数组元素逐个 join,非字符串 JSON 序列化);
  • toExceptionEvents:把event.exceptions中的异常名、消息与堆栈单独拆成kind: "exception"事件;
  • toRealtimeLogEntries:过滤掉request类事件,把日志与异常合并成面向界面的LogEntry[](异常带异常名前缀,如TypeError: xxx)。

此外,DynamicWorkerTail还会把每条结构化事件console.log到宿主 Worker 的日志,便于在 Workers 控制台做整体观测(logging.ts)。

5.2 LogSession:Durable Object 日志中转

LogSession extends DurableObject(logging.ts)提供两个 RPC 方法:

export class LogSession extends DurableObject { private waiter: LogWaiter | null = null; async addLogs(logs: LogEntry[]) { if (this.waiter) { this.waiter.addLogs(logs); } } async waitForLogs(): Promise<LogWaiter> { this.waiter = new LogWaiter(); return this.waiter; } }

这里采用经典的"消费者先就位,生产者再投递"的实时模型:宿主在调用动态 Worker 前先waitForLogs()创建LogWaiter(一个RpcTarget,即跨隔离区可调用对象);Tail Worker 稍后执行addLogs()时把日志直接推给等待者。LogWaiter.getLogs(timeoutMs)(logging.ts)在日志已到达时立即返回,否则挂起最多timeoutMs毫秒,被addLogs唤醒时清除定时器并 resolve。

也就是说,"实时回流"并不是通过 HTTP 长连接或流式响应实现,而是通过RPC 握手 + 等待/唤醒语义:执行完成后宿主一次性await logWaiter.getLogs(1000)收走这段时间内累积的所有日志。这也是 Dynamic Worker Loaders 生态中推荐的跨 Worker 数据交换姿势——利用 Durable Object 作为 RPC 枢纽。

LogSession需要在 wrangler.jsonc 中注册并声明迁移:

"durable_objects": { "bindings": [{ "name": "LogSession", "class_name": "LogSession" }] }, "migrations": [{ "tag": "v1", "new_classes": ["LogSession"] }]

同时从 src/server.ts 可见宿主 Worker 直接export { DynamicWorkerTail, LogSession } from "./logging",让加载器上下文能拿到这两个运行时导出(contextExports.DynamicWorkerTail(...)runtimeExports.LogSession.getByName(...))。

六、GitHub 仓库导入:把任意开源 Worker 拉进编辑器

客户端支持从任意公开 GitHub 仓库导入源码,服务端实现位于 src/github.ts,对外暴露POST /api/github

parseGitHubUrl负责解析 URL:只接受github.com主机名,支持仓库根路径与tree分支路径两种形式(如https://github.com/owner/repohttps://github.com/owner/repo/tree/branch/path),分支默认main

if (parts.length > 3 && parts[2] === "tree" && parts[3]) { branch = parts[3]; path = parts.slice(4).join("/"); }

fetchGitHubDirectory递归调用 GitHub Contents API(/repos/{owner}/{repo}/contents/{path}?ref={branch},请求头带Accept: application/vnd.github.v3+json与自定义 User-Agent),对目录继续下钻、对文件经download_url拉取原文,最终产出"相对路径 → 内容"的映射并保留目录层级。

导入完成后,客户端(client.tsx)会检查结果是否包含package.json,若没有则仿照服务端逻辑推断主入口并自动补一份{ name: "imported-worker", main: <入口> },随后applyFiles把整个文件树载入编辑器标签页,状态栏提示Imported N file(s)

七、客户端编辑器与实时结果面板

客户端是典型的"瘦前端 + 厚后端"结构(client.tsx),组件与能力对应 README 的 Client-side 列表:

  • Tab 键缩进TextareaonKeyDown拦截 Tab,手动插入两个空格并恢复光标位置(client.tsx);
  • 文件管理Add file弹窗新增文件(.json文件自动初始化为{}),标签页上的×可删除文件(最后一个文件禁止删除),package.json不可删除;
  • 内置示例EXAMPLES数组内置 5 个可直接运行的示例——Simple Worker(最小 fetch handler)、Multi-file Worker(跨模块 import)、JSON Config(import JSON 资源)、With Env Bindings(读取注入的API_KEY/DEBUG)、API Router(路径路由 + 404 响应),覆盖多文件、资源导入、环境变量、路由等典型场景;
  • Bundle / Minify 开关:勾选状态随请求体options透传给服务端createWorker,默认bundle = trueminify = false
  • 版本快照snapshotFiles对文件集做 JSON 快照,runWorker只在快照变化时递增workerVersion并一并提交,供服务端(或未来的缓存失效策略)参考;
  • 结果面板:按Response (status)Console (N logs)Timing (cold/warm)Bundle Info四块展示。prettyBody对 JSON 响应体做美化,getContentType显示Content-Type;Console 按error/warn/其他级别用不同前缀(/!/)与配色渲染;Timing 面板展示 Build / Load / Run / Total 四个毫秒数;Bundle Info 展示Main入口、模块清单(彩色标签)与打包Warnings

八、部署与配置要点

生产部署执行:

npm run deploy # 即 vite build && wrangler deploy

wrangler.jsonc 中有几处值得留意的配置:

{ "assets": { "not_found_handling": "single-page-application", "run_worker_first": ["/api/*"] }, "observability": { "enabled": true } }
  • assets:把 Vite 构建出的前端静态资源托管在 Workers 上,not_found_handling设为 SPA 回退;run_worker_first声明/api/*的请求先由 Worker 处理,避免静态资源路由抢先命中 API;
  • observability.enabled: true:开启 Workers 可观测性,与DynamicWorkerTail里的console.log(structuredEvent)呼应,保证整条日志管道在控制台可追踪;
  • compatibility_flags: ["nodejs_compat"]:启用 Node.js 兼容层,满足worker-bundler运行时打包对 Node 生态 API 的依赖。

整个示例依赖关系(见 package.json)包括:@cloudflare/worker-bundler(运行时打包引擎)、@cloudflare/kumo(UI 组件库,提供 Button/Dropdown/Surface/Textarea 等)、@phosphor-icons/react(图标)、agents(本仓库的 Agents SDK)、React 19 与 Tailwind CSS 4。

九、延伸:这套链路能复用到哪里

Dynamic Workers Playground 的本质,是把"代码即输入"的产品化范式在 Cloudflare Workers 上做了一次完整闭环演示。从源码结构可以推断,以下模式都可直接借鉴:

  • 插件 / 扩展运行时:用createWorker把用户脚本打包成隔离的 Worker,用worker_loaders做按内容寻址的缓存,天然获得版本化与冷热启动管理;
  • 日志回传统一通道:Tail Worker + Durable Object(getByName+ RPC 等待/唤醒)不依赖流式协议即可实现"执行结束一次性回收日志",若换成 SSE/WebSocket 也可平滑演进为真正的实时流;
  • Env 注入与配置继承compatibilityDate/compatibilityFlags/env在加载器工厂里统一注入,让动态 Worker 与宿主保持一致的运行时语义;
  • GitHub 内容导入:Contents API 递归拉取 + 入口推断 + 自动补package.json,稍作扩展即可做成"URL 即 Worker"的分享体验。

如需深入了解底层机制,可继续阅读仓库内 packages/worker-bundler 的类型定义与实现,以及示例目录下的 server.ts、logging.ts、github.ts 与 client.tsx。本地起服务后,在浏览器里改几行代码、导入一个真实仓库再点 Run Worker,即可直观感受"源码 → 打包 → 加载 → 执行 → 日志与耗时回流"的完整链路。

【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询