分裂中的NodeJS模块:为什么CommonJS和ES模块无法相处? - Dan Fabulich


自从Node诞生以来,Node模块就被编写为CommonJS模块。我们require()用来导入它们。当实现供他人使用的模块时,我们可以exports通过设置定义“命名导出”:

module.exports.foo = 'bar'

或通过设置定义“默认导出”:

module.exports = 'baz'

在Node.js的14版本中,有两种脚本:有老式的CommonJS(CJS)和新型的ESM(又名MJS)。

  • CJS使用require()、module.exports;
  • ESM使用import、export。

可以从ESM调用CJS,反之亦然,但这很麻烦。
以下是规则:
  • 您不能使用require()调用ESM;您只能使用import调用ESM,如下所示:import {foo} from 'foo'
  • CJS也不能使用静态import调用上述ESM语句。
  • CJS可以使用异步动态功能import()来使用ESM,但是与同步require相比,这很麻烦。
  • ESM可以使用 import CJS脚本,但是只能使用“默认导入”语法:import _ from 'lodash',而不是“命名导入”语法:import {shuffle} from 'lodash',如果CJS使用命名导出,则很麻烦。
  • ESM甚至可以使用命名出口也可以使用 require() 调用CJS脚本,但是通常不值得麻烦,因为它需要更多样板,而且最糟糕的是,Webpack和Rollup等捆绑软件不/不知道如何使用ESM脚本实现require()调用。
  • 默认为CJS。您必须选择进入ESM模式。通过重命名你的脚本.从js改为.mjs,这样可选择加入ESM模式。或者,您可以在package.json设置"type"为 "module",然后通过将脚本从.js重命名为.cjs来选择退出ESM 。(您甚至可以通过在package.json文件中放置一个单行{"type": "module"} 来调整单个子目录。)

这些规则很痛苦。更糟糕的是,对于许多用户,尤其是Node的新手来说,这些规则是难以理解的。
Node生态系统的许多观察者推测,这些规则是由于领导力失败,甚至是对ESM的敌意所致。但是,正如我们将看到的那样,所有这些规则都有其充分的理由,这将使将来很难打破这些规则。
最后,我将为库包作者提供三项指导原则:
  • 为你的库包提供CJS版本
  • 为您的CJS提供一个薄的ESM包装器
  • 将exports映射添加到您的package.json

一切都会好的。

ESM和CJS是完全不同的
在CommonJS中,require()是同步的;它不返回承诺或调用回调。require()从磁盘(或什至从网络)读取,然后立即运行脚本,脚本本身可能会产生I / O或其他副作用,然后返回在module.exports上设置的任何值。
在ESM中,模块加载器以异步阶段运行。在第一阶段,它解析脚本以检测对import和export的调用,无需运行导入的脚本。在解析阶段,ESM加载程序可以立即检测到命名导入中的错字并引发异常,而无需实际运行依赖项代码。
然后,ESM模块加载器异步下载并解析您导入的所有脚本,然后异步下载您导入的脚本,从而构建依赖关系的“模块图”,直到最终找到一个不导入任何内容的脚本。最后,允许该脚本执行,然后允许运行依赖该脚本的脚本,依此类推。
ES模块图中的所有“兄弟”脚本都是并行下载的,但是它们会按顺序执行,并由加载程序规范保证。

CJS是默认设置,因为ESM更改了很多内容
ESM更改了JavaScript中的许多内容。ESM脚本默认使用严格模式(use strict),它们this不引用全局对象,作用域的工作方式不同,等等。
这就是为什么即使在浏览器中<script>标签也默认为非ESM的原因;您必须添加一个type="module"属性以选择进入ESM模式。
将默认值从CJS切换为ESM将在向后兼容性方面取得重大突破。(Deno是Node的热门新替代品,它使ESM成为默认值,但结果是其生态系统从零开始。)