专栏名称: 前端大全
分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯
目录
相关文章推荐
完美Excel  ·  让deepseek实现从Word文档自动提取 ... ·  3 天前  
Excel之家ExcelHome  ·  6个Excel序号技巧,新手必会 ·  2 天前  
Excel之家ExcelHome  ·  五个公式一枝花,月入三千都靠他 ·  4 天前  
Excel之家ExcelHome  ·  Excel算年龄,DATEDIF会不会? ·  3 天前  
完美Excel  ·  初试deepseek ·  5 天前  
51好读  ›  专栏  ›  前端大全

CommonJS 和 ES Module 终于要互相兼容了???

前端大全  · 公众号  ·  · 2024-06-03 11:50

正文

在现代 JavaScript 开发中, ECMAScript Module 已经逐渐成为了公认的业界标准。 ESM 被引入 Node.js 以来,它的异步加载特性和模块解析逻辑广受大家好评。

然而,由于历史原因,很多既有代码和第三方库仍依赖于 CommonJS 模块系统,然而因为 ESM 的异步加载的设计,两个模块化方案一直是无法共存的,这也成了很多开发者的一大痛点。

最近, joyeecheung 提交的一个关键的 Pull Request (https://github.com/nodejs/node/pull/51977) 来解决这个问题。

在开始介绍前,我们先回顾一下 JavaScript 的两大模块化方案: CJS ESM

CJS 和 ESM 的前世今生

JavaScript 的世界里,模块化是构建大型应用程序的基础。模块化可以帮助开发者在不影响全局命名空间的前提下管理代码,便于功能分离、代码复用和依赖管理。在 Node.js 和浏览器环境中,有两种主流的模块系统: CommonJS(CJS) ECMAScript Module(ESM)

CommonJS Node.js 原生支持的模块系统,起初为了满足服务端模块化的需求而被引入。 CJS 使用 require 函数来加载模块,用 module.exports exports 对象将代码暴露为模块。 CommonJS 模块的特点是同步加载,这意味着代码会在模块被加载完成后立即执行:

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

// app.js
const math = require('./math.js');
console.log(math.add(017)); // 打印出 17

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

ESM 是现代 JavaScript 的官方标准模块系统,也被最新版本的浏览器原生支持。与 CommonJS 不同,它们设计成静态的,这意味着你不能在运行时动态地加载或创建模块。 ESM 使用 import export 语句进行模块的导入和导出,支持异步加载:

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

// app.js
import { add } from './math.js';
console.log(add(017)); // 打印出17

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

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

由于 ESM 是在 Node.js 中提供支持的,所以我们可以 import cjs ,但不可能 require(esm) 。这种 ERR_REQUIRE_ESM 的挫败感困扰着许多人,并且可能是 Node.js 生态系统中浪费时间的主要原因。

如果包作者想要确保 CJS ESM 用户都可以使用他们的包,他们要么必须继续将其模块作为 CJS 发布,要么将 CJS ESM 版本即作为双模块发布(这可能会导致一些问题,但现在这是一种非常常见的做法)。

同时,许多转译器(例如 TypeScript 编译器)仍然配置为生成 CJS 代码作为其最终输出。这些转译器的用户使用 ESM 语法编写代码,但他们不一定知道他们的代码最终由 Node.js 作为 CJS 运行。当他们的代码使用真正的 ESM 第三方模块(无法 require )时,他们会看到一个 ERR_REQUIRE_ESM 。这可能会非常令人困惑,因为他们可能假设他们的代码是作为真正的 ESM 运行的。

为啥不能兼容?

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

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

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

但这是一种文档和其他交流方式有误导作用的情况 - 也许它们只在谈论在 Node.js ESM 中发生的事情,而不是 ESM 本身被设计成什么样的。去年,当 joyeecheung 阅读 V8 代码来修复内存泄漏问题时,偶然发现 ESM 本身并不是真正设计成无条件异步的。而是设计成只在条件下异步 - 只有当代码中包含顶级 await 时才会异步。

那么,对 require() 至少支持不包含顶级 await 的 ESM 当然就没毛病了。虽然一些库可能有合理的理由使用顶级 await ,但这可能并不会那么常见。

的确,当 joyeecheung 后来在 npm 注册表中对 Top 影响力的仅提供 ESM 支持的包进行 require(esm) 测试时,测试的约 30 个包中没有一个包含顶级 await - 并且在 require() 中支持同步模块可能已经足够解决生态系统中的许多头痛问题。

早期的探索与尝试

同步 ESM 的支持其实也经历了长期的讨论、设计和试验。早在 2019 年, Node.js 社区就开始探讨如何支持 ESM CommonJS 之间的互操作性。期间,不少开发人员提交了 Pull Requests ,提出不同的实现方案和改进措施。

在那个时候,一个具有里程碑意义的 PR 讨论集中在如何在 Node.js 中支持 .mjs 后缀的文件,以及如何实现一个双模块系统,可以同时支持 CommonJS ESM

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

这个 pull request 试图通过在加载器中循环事件来处理顶级 await ,但它的处理方式是不安全的,这就是它被关闭的原因。

在规范方面,基于语法的 ESM 同步评估的理论基础在 2019 年已经确定。随着时间的推移, Node.js 中似乎发展出了一种关于 “ ESM 是异步的, CJS 是同步的,所以 CJS 不能加载 ESM ” 的神话,而在标准机构中, ES 规范特别注意保证 ESM 只是有条件的异步, W3C 规范使用它确保 Service Workers 只允许同步模块评估。如果规范中基于语法的同步性得到了更广泛的认知,那么在 2019 年后可能会有更多的尝试,文档也不会像无条件地谈论 ESM 是异步的。

支持同步 require(esm)

在去年年末, joyeecheung 发现根据语法, ESM 可以是同步的,而且只有 Node.js 把异步性投入到加载过程中后,于是 joyeecheung GeoffreyBooth 开始讨论重新启动同步 require(esm)







请到「今天看啥」查看全文