最近网上看到一个挺有意思的消息,让我不禁想跟大家聊聊。那就是 ECMAScript Module
和 CommonJS
的互操作性问题终于有望解决了!是不是很惊喜,这可是困扰咱们开发者多年的老大难问题啊。说到这儿,你是不是又想起了 ERR_REQUIRE_ESM
?
CJS 和 ESM 的前世今生
在 JavaScript
的世界里,模块化是构建大型应用程序的基础。模块化可以帮助开发者在不影响全局命名空间的前提下管理代码,便于功能分离、代码复用和依赖管理。CommonJS
和 ECMAScript Module
就是这两大模块化方案的代表。
CommonJS(CJS)
CommonJS
是 Node.js
原生支持的模块系统,使用 require
函数加载模块,用 module.exports
或 exports
对象将代码暴露为模块。其特点是同步加载,意味着代码会在模块被加载完成后立即执行:
`// 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
的官方标准模块系统,使用 import
和 export
语句进行模块的导入和导出,支持异步加载:
`// 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
年就开始探讨如何支持 ESM
和 CommonJS
之间的互操作性。期间,不少开发人员提交了 Pull Requests
,提出不同的实现方案和改进措施。
当时一个具有里程碑意义的 PR
讨论集中在如何在 Node.js
中支持 .mjs
后缀的文件,以及如何实现一个双模块系统,可以同时支持 CommonJS
和 ESM
。不过,这些尝试都因为各种技术和安全性问题未能成功。
支持同步 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"
的包。尽管如此,这已经为许多困扰开发者的问题提供了解决方案。