跳到主要内容
Deno 2.4 已发布,包含 deno bundle、字节/文本导入、稳定版 OTel 等新特性
了解更多
Progress of JavaScript being obstructed by CommonJS.

CommonJS 正在伤害 JavaScript

JavaScript,无可争议的Web开发之王,正在被其自身的历史包袱所破坏——而不是被竞争语言或革命性新技术。这个阴险的破坏者正是 CommonJS,一个我们容忍了太久的过时模块系统。

CommonJS 的崛起

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

2009年,Mozilla 开发者 Kevin Dangoor 发出了号召。在《服务器端 JavaScript 需要什么》一文中,他阐述了新兴的服务器端 JS 领域所缺少的大部分内容,包括一个模块系统。

JavaScript 需要一种标准方式来包含其他模块,并让这些模块存在于独立的命名空间中。实现命名空间有很多简单的方法,但没有标准的编程方式来加载模块(一次!)。这非常重要,因为服务器端应用程序可以包含大量代码,并且很可能会混合搭配符合这些标准接口的部分。

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

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

提议的 CommonJS 语法(require()module.exports 等)与客户端 JavaScript 不同。这是有意为之。Dangoor 在 2009 年的 CommonJS 谷歌小组 中的消息清楚地表明了他将 CommonJS 与浏览器 JavaScript 区分开来的意图。

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

除了 Node.js,其他几个早期的服务器端 JavaScript 运行时也采用了 CommonJS,如 Flusspferd、GPSEE、Narwhal、Persevere、RingoJS、Sproutcore 和 v8cgi(其中大部分由 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 模块假定它们将通过网络而非文件系统获取数据,从而提供更好的性能和用户体验。

既然模块加载系统已内置于语言中,大家都会同意使用它,这样我们就可以把精力集中在更高级、更重要的问题上,对吧?

Anakin Padme 4 panel meme about ESM vs. CommonJS

……对吧?

Node 决定同时支持 CJS 和 ESM

ES 模块和 CommonJS 搭配起来,就像老湾调料和香草冰淇淋一样。

— Myles Borins,摘自关于《模块、模块、模块》的演讲

(我来自马里兰州,所以这听起来很棒。)

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

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

发布同时支持 esm 和 cjs 的模块 “发布同时支持 esm 和 cjs 的包简直是噩梦” — Wes Bos

其他模块作者在使用 dnt 支持 CommonJS 和 ESM 方面取得了成功。只需用 TypeScript 编写模块,此构建工具就会将其转换为 Node.js,并生成 ESM/CommonJS/TypeScript 声明文件以及 package.json

显然,在2023年支持 CommonJS 已成为一个不容忽视的巨大问题。现在是我们埋葬 CommonJS 并迈向纯 ESM 未来的时候了。

再见,感谢所有的 requires

我们设想这样一个未来:开发者在安装模块后,无需构建步骤即可在 Node.js 或浏览器中运行代码。

— Myles Borins,ES 模块的当前实现状态和规划 (2017)

2009年,CommonJS 正是 JavaScript 所需。该小组解决了一个棘手的问题,并强行推行了一种解决方案,至今仍每天被使用数百万次。

但随着 ESM 成为标准,并且焦点转向云原生(边缘、浏览器和无服务器计算),CommonJS 显然已无法胜任。ESM 对开发者来说是一个更好的解决方案,因为他们可以编写符合浏览器规范的代码,对于用户而言也能获得更好的最终体验。

加入讨论。

不要错过任何更新 — 在 Twitter 上关注我们