Read OSS

从源码到边缘网络:构建系统与部署流水线

中级

前置知识

  • 本系列第 1、2 篇文章
  • esbuild 基础知识(插件、conditions、targets)
  • 熟悉 Cloudflare Workers 上传 API

从源码到边缘网络:构建系统与部署流水线

运行 wrangler deploy 时,你的 TypeScript Worker 并不会直接传输到 Cloudflare 网络,而是经历一条多阶段流水线:esbuild 对源码进行转换和打包,自定义插件负责解析 Workers 专属模块,模块收集器按 MIME 类型对输出结果分类,最终将所有内容打包成一个多部分 FormData 上传请求,并附上携带 binding、兼容性设置和部署配置的元数据 blob。

本文将逐步追踪这条流水线的每一个环节——从让 Workers 打包有别于浏览器打包的 esbuild 配置,到五个自定义 esbuild 插件,再到在上传前执行远端配置差异对比的完整 deploy() 函数。

bundleWorker() 与 noBundleWorker()

Wrangler 支持两种打包模式。主路径使用位于 packages/wrangler/src/deployment-bundle/bundle.ts#L149bundleWorker(),它会调用 esbuild 并走完完整的插件流水线。另一种方案是 noBundleWorker(),位于 packages/wrangler/src/deployment-bundle/no-bundle-worker.ts#L9-L32,它完全跳过 esbuild,仅扫描额外模块。

当用户传入 --no-bundle 时会触发 noBundleWorker()——通常是因为 Worker 已由其他工具打包完毕,或本身是无需转换的单文件 Worker。即便如此,它仍会执行 findAdditionalModules() 来发现 WASM 文件、文本资源以及其他需要包含在上传包中的模块类型。

共享的 esbuild 配置定义为 COMMON_ESBUILD_OPTIONS,位于 packages/wrangler/src/deployment-bundle/bundle.ts#L46-L52

export const COMMON_ESBUILD_OPTIONS = {
    target: "es2024",
    supported: { "import-source": true },
    loader: { ".js": "jsx", ".mjs": "jsx", ".cjs": "jsx" },
} as const;

target: "es2024" 值得关注——workerd 的 V8 原生支持 ES2024 特性,因此无需将现代语法降级转换。.jsjsx 的 loader 映射让普通 .js 文件无需更名为 .jsx 也能使用 JSX,与 Workers 生态中的惯例保持一致。

flowchart TD
    SRC["Worker source (TS/JS)"] --> DECISION{--no-bundle?}
    DECISION -->|No| BUNDLE["bundleWorker()<br/>Full esbuild pipeline"]
    DECISION -->|Yes| NOBUNDLE["noBundleWorker()<br/>Module scan only"]
    BUNDLE --> RESULT["BundleResult<br/>modules + dependencies + sourcemap"]
    NOBUNDLE --> RESULT

Workers 构建条件:workerd、worker、browser

packages/wrangler/src/deployment-bundle/bundle.ts#L69-L76 中的 getBuildConditions() 函数设置了 esbuild 的 conditions,用于决定 package.jsonexports 字段的解析方式:

export function getBuildConditions() {
    const envVar = getBuildConditionsFromEnv();
    if (envVar !== undefined) {
        return envVar.split(",");
    } else {
        return ["workerd", "worker", "browser"];
    }
}

条件的顺序至关重要。当一个库的 package.json 使用条件导出时:

{
  "exports": {
    ".": {
      "workerd": "./dist/workerd.js",
      "worker": "./dist/worker.js",
      "browser": "./dist/browser.js",
      "default": "./dist/node.js"
    }
  }
}

esbuild 会优先选择 workerd(最具针对性的 Cloudflare 运行时目标),其次是 worker(通用 Web Worker),再次是 browser,最后才是 default。这确保了针对 workerd 运行时做过优化的库代码路径能被优先使用。

条件 用途
workerd Cloudflare Workers 运行时——最具针对性
worker 通用 Web Worker 标准
browser 浏览器环境(同构代码的回退选项)
default 始终激活(esbuild 内置)
import ESM 语法时激活(esbuild 内置)

提示: 可以通过 WRANGLER_BUILD_CONDITIONS 环境变量覆盖条件配置。在测试 Worker 对不同库导出路径的行为时,这个功能非常实用。

自定义 esbuild 插件

bundleWorker() 注册了若干自定义 esbuild 插件,其中有三个值得重点关注:

Node.js 兼容性插件

packages/wrangler/src/deployment-bundle/esbuild-plugins/nodejs-plugins.ts 模块通过 unenv 包提供 Node.js API 兼容 shim。当启用 nodejs_compatnodejs_compat_v2 兼容性标志时,这些插件会拦截对 node:* 内置模块的导入,并将其重定向到能在 Workers 运行时中正常工作的 polyfill 实现。

Cloudflare 内部模块解析

packages/wrangler/src/deployment-bundle/esbuild-plugins/cloudflare-internal.ts 插件负责解析 cloudflare:* 虚拟模块(如 cloudflare:emailcloudflare:sockets)以及 workerd:* 导入。这些并非磁盘上真实存在的文件,而是由运行时提供的模块,需要被标记为 external,以防 esbuild 尝试将其打包进去。

Config Provider 插件

packages/wrangler/src/deployment-bundle/esbuild-plugins/config-provider.ts 在构建时注入 wrangler 配置值,让代码可以在编译期访问配置,无需承担运行时开销。

flowchart LR
    ESBUILD["esbuild"] --> NODEJS["nodejs-plugins<br/>Node.js polyfills via unenv"]
    ESBUILD --> CF["cloudflare-internal<br/>cloudflare:* & workerd:* modules"]
    ESBUILD --> CP["config-provider<br/>Build-time config injection"]
    ESBUILD --> BR["buildResultPlugin<br/>Captures initial build result"]
    ESBUILD --> LOG["log-build-output<br/>Bundle size reporting"]

bundleWorker() 还会在开发模式下注入 middleware loader。在开发环境中,Worker 会被包裹上用于请求体排空、定时事件测试以及 JSON 错误格式化(用于 Miniflare 中友好的错误页面)的 middleware。

模块收集与 MIME 类型映射

esbuild 生成输出后,模块收集器会按类型对每个输出文件进行分类。packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts#L22-L32 中定义了如下映射关系:

模块类型 MIME 类型
esm application/javascript+module
commonjs application/javascript
compiled-wasm application/wasm
buffer application/octet-stream
text text/plain
python text/x-python
python-requirement text/x-python-requirement

这个映射至关重要,因为 Cloudflare Workers API 依赖多部分上传中每个 part 的 Content-Type 头来判断如何处理对应模块。如果 WASM 文件使用了错误的 MIME 类型,运行时将无法加载。

createModuleCollector() 函数以 esbuild 插件的形式工作——它拦截 esbuild 的模块解析过程,追踪哪些文件被包含在 bundle 中,并在构造上传表单之前完成分类。

createWorkerUploadForm():构建多部分上传

createWorkerUploadForm() 函数负责构造发送给 Cloudflare API 的 FormData 对象。上传格式是一个多部分表单,包含以下内容:

  1. metadata — 一个 JSON blob,包含 Worker 的 binding、兼容性日期/标志、使用模式、placement、tail consumers、可观测性设置以及资源配置
  2. 主模块 — 作为第一个命名 part 的入口脚本
  3. 附加模块 — 每个额外模块(WASM 文件、文本资源、子模块)作为独立的命名 part,并附上对应的 MIME 类型
  4. Source map — 用于错误堆栈追踪映射的可选 source map 文件

该函数按类型提取 binding——包括 plain_textjsonsecret_textkv_namespacedurable_object_namespacequeuer2_bucketd1 等众多类型——然后将它们转换为 API 期望的元数据格式,详见 packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts#L115-L134

flowchart TD
    WORKER["Worker init data"] --> FORM["createWorkerUploadForm()"]
    BINDINGS["Bindings"] --> FORM
    FORM --> META["metadata (JSON):<br/>bindings, compat date,<br/>placement, limits"]
    FORM --> MAIN["main module:<br/>application/javascript+module"]
    FORM --> MODS["additional modules:<br/>WASM, text, etc."]
    FORM --> SMAP["source maps"]
    META --> FD["FormData"]
    MAIN --> FD
    MODS --> FD
    SMAP --> FD

deploy() 函数:端到端流程

packages/wrangler/src/deploy/deploy.ts 中的完整部署流程将所有环节串联起来:

flowchart TD
    START["deploy(props)"] --> CHECK["Check if worker exists"]
    CHECK --> DIFF["Fetch remote config<br/>Compare with local"]
    DIFF --> WARN{Destructive changes?}
    WARN -->|Yes| CONFIRM["Prompt user to confirm"]
    WARN -->|No| BUNDLE
    CONFIRM --> BUNDLE["Bundle worker<br/>(bundleWorker or noBundleWorker)"]
    BUNDLE --> FORM["createWorkerUploadForm()"]
    FORM --> UPLOAD["Upload to Workers API"]
    UPLOAD --> ROUTES["Publish routes/custom domains"]
    ROUTES --> QUEUES["Reconcile queue consumers"]
    QUEUES --> VERSION["Create version/deployment"]
    VERSION --> DONE["Log success + URL"]

packages/wrangler/src/deploy/deploy.ts#L448-L460 中的配置差异对比步骤是一道安全防线。如果 Worker 上次是从 Cloudflare 控制台(而非 Wrangler)部署的,该函数会拉取远端配置并与本地配置进行比对。若本次部署会覆盖掉在控制台手动修改的内容(即存在破坏性变更),系统会向用户发出警告并要求确认。

命令定义本身位于 packages/wrangler/src/deploy/index.ts#L36-L42,采用了第 2 篇文章中介绍的 createCommand() 模式:

export const deployCommand = createCommand({
    metadata: {
        description: "🆙 Deploy a Worker to Cloudflare",
        owner: "Workers: Deploy and Config",
        status: "stable",
        category: "Compute & AI",
    },
    // ...
});

提示: wrangler deploy--dry-run 标志会完整执行打包流水线并构造上传表单,但不会实际发送请求。在 CI 流水线中验证 Worker 能否正常打包却又不想触发真实部署时,这个选项非常有价值。

下一步

至此,我们已经完整追踪了从源码到 Cloudflare 边缘网络的全部路径。但还有另一条平行的本地开发路径,它完全绕开了 Wrangler——@cloudflare/vite-plugin 提供了一流的 Vite 集成,它使用相同的 Miniflare 运行时核心,但采用了截然不同的编排层。第 6 篇文章将深入探讨 Vite 插件,以及确保两套工具以完全相同方式解析 wrangler.toml 的共享配置系统。