JavaScript,无可争议的Web开发之王,正在被破坏——不是被竞争对手的语言或革命性的新技术所破坏,而是被它自己过去的包袱所破坏。这个阴险的破坏者就是CommonJS,一个我们已经容忍了太久的古董模块系统。

CommonJS的兴起

在发明大约 15 年后,JavaScript 开始从浏览器扩展到服务器。更大的项目正在用这种语言构建,JavaScript需要一种更好的方法来处理大量的源代码。它需要模块化。

2009年,Mozilla开发者Kevin Dangoor发出了号召。在“服务器端JavaScript需要什么”中,他列出了服务器端JS的新兴领域所缺少的许多内容,包括模块系统。

JavaScript 需要一种标准的方式来包含其他模块,并使这些模块存在于谨慎的命名空间中。有一些简单的方法来执行命名空间,但没有标准的编程方法来加载模块(一次!这非常重要,因为服务器端应用程序可以包含大量代码,并且可能会混合和匹配满足这些标准接口的部分。

——Kevin Dangoor,《服务器端JavaScript需要什么》(2009)

在一周内,224人加入了当时被称为ServerJS的Google小组,其中包括npm创始人Issac Schlueter和Node.js创建者Ryan Dahl(这是他将Node介绍给该小组的地方)。这个邮件列表将继续规范CommonJS的第一个版本,该模块系统成为Node的一部分。

提议的 CommonJS 语法( require()module.exports 等)看起来不像客户端 JavaScript。这是设计使然。Dangoor将CommonJS与浏览器JavaScript拉开距离的意图从他在2009年CommonJS谷歌集团的信息中得到了明确:

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

除了 Node.js其他几个早期的服务器端 JavaScript 运行时也采用了 CommonJS,如 Flusspferd、GPSEE、Narwhal、Persevere、RingoJS、Sproutcore 和 v8cgi(大多数由核心 CommonJS 组构建)。

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

回想起来,在我看来,CommonJS的目标是(或者至少应该是)发现Node,并实现我们在这里构建的东西。犯了一些错误,因为事后诸葛亮并没有朝着你想要的方向发展,但总的来说,我认为整个 CommonJS 项目可以被认为是成功的。 JavaScript 需要一种标准的方式来包含其他模块,并使这些模块存在于谨慎的命名空间中。有一些简单的方法来执行命名空间,但没有标准的编程方法来加载模块(一次!这非常重要,因为服务器端应用程序可以包含大量代码,并且可能会混合和匹配满足这些标准接口的部分。

— Issac Schlueter,评论打破CommonJS标准化僵局(2013)

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

  • 模块加载是同步的。每个模块都按照所需的顺序逐个加载和执行。
  • 难以 tree-shaking,可以删除未使用的模块并最小化捆绑包大小。 不是浏览器本机。您需要捆绑器和转译器来使所有这些代码在客户端工作。使用 CommonJS,您要么需要执行大型构建步骤,要么为客户端和服务器编写单独的代码。 到2013年,CommonJS小组开始逐渐减少。但到了那一年,负责监督核心JavaScript语言更新的TC39委员会已经在研究CommonJS模块的后继者:ECMAScript模块。

ECMAScript 模块是 Web 优先的

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

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

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

anakin-meme

...Right?

Node决定同时支持CJS和ESM

ES 模块和通用 JS 像老湾调味料和香草冰淇淋一样搭配在一起 JavaScript 需要一种标准的方式来包含其他模块,并使这些模块存在于谨慎的命名空间中。有一些简单的方法来执行命名空间,但没有标准的编程方法来加载模块(一次!这非常重要,因为服务器端应用程序可以包含大量代码,并且可能会混合和匹配满足这些标准接口的部分。

— 迈尔斯·博林斯,来自模块模块模块的演讲

Borins是Node“模块团队”的开发人员之一,负责在Node中实现ES模块。尽管成功地将ESM添加到Node中,但该团队无法就ESM和CJS之间的互操作性达成明确的共识。然而,Node无法撕掉CJS,因为它嵌入得如此之深。这意味着互操作性问题被推给了包作者。

以下是支持 ESM 和 CJS 所需的模块 package.json 片段:

“发布在 ESM 和 CJS 中工作的软件包真是一场噩梦” — 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 作为标准,焦点转向云原语(边缘、浏览器和无服务器计算),CommonJS 根本无法解决这个问题。对于开发人员来说,ESM 是更好的解决方案,因为他们可以编写与浏览器兼容的代码,对于获得更好最终体验的用户来说也是如此。