Skip to content

Cloudflare 重写 Workers 模块注册表,强化 Node.js 兼容 ​

译注:本文编译自 Cloudflare 官方博客,原文标题为《How we rebuilt Cloudflare Workers' module registry for Node.js compatibility》,作者 Logan Gatlin 与 James Snell,发布于 2026 年 9 月 9 日。原文链接见文末。

Cloudflare 宣布重写了 workerd(Workers 运行时的核心开源组件)中的模块注册表(module registry),使其更快、更符合标准,并更贴近 Node.js 的模块注册表行为。开发者现在可以通过启用 new_module_registry 兼容性标志来使用这一新实现。

背景:为什么需要重写 ​

过去几年,Cloudflare 持续为 Workers 运行时补充 Node.js 运行时 API 支持。官方表示,Workers 运行时现已支持在 Serverless 场景中可能用到的所有 Node.js 稳定 API,且这些 API 默认启用,允许部署更大的 Node.js 应用(所有套餐最高 64 MiB,并取消了压缩包体积限制)。

但仅有 API 兼容并不够——Node.js 应用还依赖运行时如何解析、加载和缓存模块。ESM、CommonJS 和 WebAssembly 都是可以在 Worker 代码中导入的模块类型,而负责处理这一切的系统就是模块注册表。

启用方式 ​

在 Worker 中启用 new_module_registry 兼容性标志即可:

json
{
  "compatibility_flags": ["new_module_registry"]
}

启用后,以下能力生效:

  • import.meta.url、import.meta.main 和 import.meta.resolve() 均可正常工作;
  • 模块说明符(specifier)按真实 URL 解析,包括查询字符串和片段;
  • node: 内置模块无论通过何种路径访问,都解析到同一模块实例;
  • 导入属性(如 with { type: 'json' })会被正确校验;
  • 对 ES 模块调用 require() 遵循 Node.js 的 require(esm) 规则;
  • 无论由哪条加载路径触发,错误都使用一致的类和消息;
  • 模块在首次导入时惰性编译(静态或动态导入皆然);
  • WebAssembly 模块支持 source phase imports。

运行时如何加载代码 ​

部署 Worker 时,wrangler 或 Vite 会把众多文件和依赖打包成一个或多个模块再上传。默认情况下,Wrangler 使用 esbuild 将几乎所有代码打包进单个模块脚本,把相对导入和 require() 调用内联为普通函数。因此到达 workerd 时,通常已没有多少模块图需要处理,脚本可能长达数十万行。

使用 Cloudflare Vite 插件时,Vite 8 改用 Rolldown 打包,会解析导入与 npm 依赖、在必要时把 CommonJS 转为 ESM,并输出入口模块及代码分割产生的额外 chunk。这样运行时收到的是更小的、由构建生成的模块图,而非应用原始源码图。新模块注册表为 Rolldown 这类打包器减少转换、更多依赖运行时处理模块解析打开了空间。

旧实现的局限 ​

原注册表把说明符当作文件系统风格路径而非 URL 解析,由此带来一系列限制:无法干净地实现 import.meta.url;相对导入与 new URL() 的解析规则不一致;node:、cloudflare: 等协议被当作特例字符串前缀而非协议处理。此外,它会在启动时编译整个 Worker 包,无论某模块是否真被导入,并且每个 V8 isolate 都保留一份私有的完整副本。由于 Cloudflare 会为同一 Worker 运行多个 V8 isolate 副本以分摊负载,这实际上意味着同一份源码被多次编译、多份驻留内存。

新注册表以 URL 作为说明符格式,并从设计之初就纳入惰性编译与缓存共享。官方强调,现有注册表实现不会消失,已部署的 Worker 将继续照常运行。

import.meta 与 URL 语义 ​

import.meta 提供模块信息,例如模块 URL 以及是否为入口模块。import.meta.main 仅对配置为 Worker 入口的模块为 true。import.meta.resolve() 在不导入的情况下解析说明符,它是纯字符串变换,与 Node.js 和浏览器一致:不检查解析结果是否对应真实模块,对无法解析为 URL 的说明符抛出 TypeError。它按 new URL() 的方式规范化百分号编码,但不会解码已编码字符。

相对导入现在与 new URL(specifier, base) 行为一致,完整 URL 也可作为说明符。查询字符串和片段遵循浏览器的模块标识规则:指向同一源码但查询串或片段不同的说明符,会被视为真正不同的模块实例,各自拥有独立的 import.meta.url 和顶层状态副本。

导入属性与 require(esm) ​

原实现会静默忽略导入属性,违反规范。新实现中,json 是当前唯一启用的导入属性类型(相关 TC39 提案中唯一达到 Stage 4 的)。text 和 bytes 被识别但会以特定错误拒绝,而非静默忽略。除 type 外的属性键现在是硬错误,类型与模块实际类型不匹配也会报错。

对 ES 模块调用 require() 时,注册表遵循 Node.js 的 require(esm) 行为:若模块有名为 module.exports 的字符串导出,则返回该值;否则返回模块的命名空间对象。唯一的例外是 workerd 自带的 node: 内置模块。

原文链接 ​

https://blog.cloudflare.com/workers-module-registry-nodejs/