Node.js 24.15 的 require(esm) 终于 stable 了,我第一时间把它跑通了
如果你被 npm 包的 CJS/ESM 双版本发布折磨过,今天这篇文章可能是你今年最值得花 5 分钟读的前端资讯。
上周 Node.js 24.15.0 LTS 悄悄更新了一个功能:require(esm) 从实验性标记正式毕业,变成了稳定特性。这意味着——在 Node.js 环境里,CommonJS 文件终于可以直接用 require() 加载一个纯 ESM 的包了,不用再配 type: module,不用再打两套产物,不用再和 package.json 的 exports/main/module 字段较劲。
过去几年这是什么体验?
大多数 npm 库为了兼容两边,会同时打 CJS 和 ESM 两个版本,package.json 里写一堆条件导出:
{
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"exports": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
库作者累,CI 构建时间翻倍,产物包体积也大了一圈。而如果你写的应用想直接 require 一个只发 ESM 的包,抱歉,不支持。
require(esm) 解决的就是这个问题。它本质上是在 CommonJS 模块系统里嵌入了一个同步 ESM 加载器。Joyee Cheung(Node.js 模块系统的主要维护者)专门写过一篇实现细节,核心思路是给被加载的 ESM 模块加一个 __esModule 标记,然后用 module-sync 条件来协调两套导出语义。底层同步加载机制也经过重构,消除了之前和异步 import() 的竞态问题。
Node.js 24.15 LTS 这个版本特别值得关注,是因为它同时带了几个配套更新:V8 升级到 12.4(带来了 WebAssembly GC、Array.fromAsync、Set 的 union/intersection/difference 等),还有 Maglev 编译器默认启用。require(esm) 在这个节点 stable,时机刚好。
现在你具体怎么用?
第一步:确认你的 Node.js 版本 >= 24.15.0 LTS。如果用的是项目里的 nvm 或者 fnm:
nvm install 24
nvm use 24
node -v # 确认 >= 24.15.0
第二步:找一个纯 ESM 的包,直接 require 进来试试。比如 valibot(一个 TypeScript schema 验证库,只发了 ESM 版本):
// CJS 文件里直接写
const { parse } = require(valibot);
const result = parse(
[{ username: alice }],
array(object({ username: string() }))
);
console.log(result); // [{ username: alice }]
不需要 package.json 加 type: module,不需要任何实验性 flag,直接跑。
第三步:如果你的团队有祖传 CJS 代码要逐步迁移到 ESM,现在可以用 require(esm) 作为过渡:
// 旧的 CJS 模块
module.exports = { foo: 1 };
// 新的 ESM 模块(main 指向它)
export const foo = 1;
export const bar = 2;
Consumer 侧只需要升级 Node.js 版本就行,改动最小。
有没有坑?
Joyee Cheung 在实现笔记里特别提到了「faux ESM」场景:有些包表面发的是 ESM,实际上是转译过的 CommonJS(标记了 __esModule)。require(esm) 对这类包的兼容逻辑比较复杂,如果你踩到了,建议先看官方文档的兼容列表,别硬上。
另外:require(esm) 是同步的,所以动态 import() 该用的时候还是得用,它解决的是「同步加载 ESM」这个具体问题,不是替代 ESM 的动态加载能力。
怎么判断该不该迁移?
如果你的团队还在维护双版本发布的 npm 包,或者有一堆祖传 CJS 代码等着逐步换 ESM,这个版本值得第一时间上。Node.js 26(今年 10 月正式 LTS)也会带这个特性,届时会是更稳妥的生产环境选择。
现在的推荐策略:开发环境用 Node.js 24.15 跑通,生产等 10 月 Node.js 26 LTS。
评论区
登录后可评论。