CommonJS 该功成身退了

JavaScript,作为 Web 开发的基础语言正在被侵蚀 —— 不是被竞争对手的语言或革命性的新技术所破坏,而是受到来自过去的包袱的困扰。这个破坏者正是 CommonJS,这个我们已经容忍了太久的老旧模块系统。

CommonJS 的崛起

大约15年后,JavaScript 开始不再局限于浏览器,而是开始在服务器端扩展。越来越大的项目开始使用该语言,而 JavaScript 需要更好的方式来处理大量源代码。它需要模块化。

2009年,Mozilla 的开发者Kevin Dangoor发出了一声呼吁。在《服务器端 JavaScript 需要什么》一文中,他列举了服务器端 JS 这个新兴领域缺少的很多内容,包括一个模块系统。

JavaScript 需要一种标准的方式来包含其他模块,并使这些模块位于离散的命名空间中。有一些简单的方法可以实现命名空间,但没有标准的编程方式来加载一个模块(仅一次!)。这真的很重要,因为服务器端应用可能会包含大量代码,并且可能会混合匹配符合这些标准接口的部分。
Kevin Dangoor,《服务器端 JavaScript 需要什么》(2009)

在一周内,224人加入了当时名为 ServerJSGoogle 论坛,其中包括 npm 创始人 Issac SchlueterNode.js 创造者 Ryan Dahl(在这里他向团队介绍了 Node)。这个邮件列表后来起草了 CommonJS 的第一个版本,这个模块系统成为了Node的一部分。

提议的 CommonJS 语法(require()module.exports 等)看起来不像客户端 JavaScript。这是有意设计的。Dangoor 从2009年在 CommonJS Google 论坛上的留言中清楚地表达了将 CommonJS 与浏览器JavaScript区分开来的意图:

我认为服务器端代码的需求与客户端代码的需求有很大的不同,我们最好从 Python Ruby 中借鉴,而不是从 Dojo jQuery 中借鉴。

除了 Node.js,其他一些早期的服务器端 JavaScript 运行时也采用了 CommonJS,如 FlusspferdGPSEENarwhalPersevereRingoJSSproutcorev8cgi(大多数由核心 CommonJS 团队构建)。

但随着 Node.js 成为事实上的服务器端 JavaScript 运行时,而 CommonJS 成为其主要模块系统,更广泛的 CommonJS 标准化工作失去了动力。当只有一个主要运行时时,标准的需求就会减少:Node.js 的实现变成了标准。

回顾过去,我觉得 CommonJS 的目标(或者至少应该是)是发现 Node,并使我们在这里构建的东西变得可用。有些错误是可以避免的,因为回顾不会按照你希望的方向进行,但总体而言,我认为整个 CommonJS 项目都可以被视为成功。
— Issac Schlueter,评论《突破 CommonJS 标准化僵局》(2013)

尽管是默认的模块系统,但 CommonJS 也存在一些核心问题:

  • 模块加载是同步的。每个模块都是按顺序加载和执行的。
  • 难以进行tree-shake,这可以删除未使用的模块并最小化包大小。
  • 不是浏览器原生的。您需要使用打包工具和转译器才能使所有代码在客户端正常工作。使用 CommonJS,您要么需要庞大的构建步骤,要么需要为客户端和服务器端编写不同的代码。

到了2013年,CommonJS 团队开始逐渐减少。但是在那一年,负责更新核心 JavaScript 语言的 TC39 委员会已经在制定CommonJS模块的后继者:ECMAScript 模块。

ECMAScript 模块是面向 Web 的

随着 ES6 语言规范的推出,TC39 委员会最终引入了一个内置于 JavaScript 语言中的模块系统。其目标是构建一个适用于Web的单一模块加载器系统,包括异步模块加载、与浏览器兼容、静态分析和摇树优化。

ES 模块假定它们将从网络中获取数据,而不是从文件系统中获取,从而提供更好的性能和用户体验。

现在,模块加载系统已经内置于语言中,每个人都将同意使用它,这样我们就可以将精力集中在更高层次、更重要的问题上了,对吗?

Node决定同时支持CJSESM

BorinsNode ‘模块团队’ 中的开发人员之一,负责在 Node 中实现ES模块。尽管成功将 ESM 添加到 Node 中,但团队无法就ESMCJS 之间的互操作性达成明确共识。然而,Node 无法摆脱 CJS,因为它已经深深嵌入其中。这意味着互操作性问题被推给了包作者。

以下是支持ESMCJS的模块的package.json的片段:

发布支持 esm cjs 的模块“发布可以在 esmcjs 中运行的包是一种噩梦” — Wes Bos

其他模块作者使用dnt成功支持了 CommonJS 和 ESM。只需在 TypeScript 中编写您的模块,此构建工具将其转换为 Node.js,生成 ESM/CommonJS/TypeScript 声明文件和 package.json

很明显,在2023年支持 CommonJS 已经成为一个无法忽视的大问题。现在是时候抛弃 CommonJS 并过渡到全面采用 ESM 的未来了。

告别 require

我们设想未来,开发人员将能够在安装模块后,在 Node.js 或浏览器中运行代码,而无需构建步骤。
— Myles Borins,《ESModules 的当前实现状态和规划》(2017)

2009年,CommonJS 正是 JavaScript 需要的。该团队解决了一个棘手的问题,并强行推出了一个解决方案,这个解决方案每天被数百万次使用。

但随着ESM成为标准,焦点转向云原生 —— Edge、浏览器和无服务器计算,CommonJS 已经不再适用。对于开发人员来说,ESM 是一个更好的解决方案,因为他们可以编写符合浏览器的代码,对于用户来说,也会获得更好的最终体验。