最近网上看到一个挺有意思的消息,让我不禁想跟大家聊聊。那就是 ECMAScript ModuleCommonJS 的互操作性问题终于有望解决了!是不是很惊喜,这可是困扰咱们开发者多年的老大难问题啊。说到这儿,你是不是又想起了 ERR_REQUIRE_ESM

CJS 和 ESM 的前世今生

JavaScript 的世界里,模块化是构建大型应用程序的基础。模块化可以帮助开发者在不影响全局命名空间的前提下管理代码,便于功能分离、代码复用和依赖管理。CommonJSECMAScript Module 就是这两大模块化方案的代表。

CommonJS(CJS)

CommonJSNode.js 原生支持的模块系统,使用 require 函数加载模块,用 module.exportsexports 对象将代码暴露为模块。其特点是同步加载,意味着代码会在模块被加载完成后立即执行:

`// math.js
function add(x, y) {
  return x + y;
}
module.exports = { add };

// app.js
const math = require(’./math.js’);
console.log(math.add(0, 17)); // 打印出 17

`

在服务器环境中,同步加载通常不是问题,因为文件大都在本地。然而,在浏览器环境中,同步加载可能会导致性能问题,因为它会阻塞浏览器的事件循环,直到脚本完全下载和解析。

ECMAScript Module(ESM)

ESM 是现代 JavaScript 的官方标准模块系统,使用 importexport 语句进行模块的导入和导出,支持异步加载:

`// math.js
export function add(x, y) {
  return x + y;
}

// app.js
import { add } from ’./math.js’;
console.log(add(0, 17)); // 打印出17

`

ESM 的设计允许浏览器优化加载和解析过程,如通过 HTTP/2 进行有效的并行加载,以及进行 tree shaking 以剔除未使用的代码,从而增强性能和效率。但是,在 Node.js 中,ESM 的异步特性与现有的大量 CommonJS 模块存在不兼容问题。

当前在 Node.js 中启用 ESM 的方法要复杂一些,因为代表性的 .js 文件扩展名默认与 CommonJS 模块关联。为了解决此问题,Node.js 允许使用 .mjs 文件扩展名或在 package.json 中明确指定 "type": "module" 属性来表示 ESM 模块。

为啥不能兼容?

自然地,人们可能会问:为什么 require() 就不能支持加载 ESM 呢?

很长一段时间以来,Node.js 项目的答案总是这样:

使用 require 来加载 ES 模块是不被支持的,因为 ES 模块是异步执行的。

这也是为什么 ERR_REQUIRE_ESM 这个错误总是让人难受的原因之一。

早期的尝试

其实,社区早在 2019 年就开始探讨如何支持 ESMCommonJS 之间的互操作性。期间,不少开发人员提交了 Pull Requests,提出不同的实现方案和改进措施。

当时一个具有里程碑意义的 PR 讨论集中在如何在 Node.js 中支持 .mjs 后缀的文件,以及如何实现一个双模块系统,可以同时支持 CommonJSESM 。不过,这些尝试都因为各种技术和安全性问题未能成功。

支持同步 require(esm)

在去年年末,joyeecheung 发现根据语法,ESM 可以是同步的。而且,只有当代码中包含顶级 await 时才会异步。这意味着我们可以支持同步 require(esm),只要确保不包含顶级 await

这就有了 joyeecheung 最近提交的关键 Pull Request

https://github.com/nodejs/node/pull/51977

这个 PR 尝试使 require(esm) 的范围保持小,并且只支持加载同步 ESM。事实证明,这在技术指导委员会(TSC)中根本不是一个有争议的想法,并且没有遭到多少争议。

目前,这个特性仍然在实验阶段,需要使用 --experimental-require-module 标志来启用。而且,只支持那些显式标记为 ESM 的模块,比如 .mjs 扩展名的文件或者在 package.json 中指定 "type": "module" 的包。尽管如此,这已经为许多困扰开发者的问题提供了解决方案。

个人笔记记录 2021 ~ 2025