JavaScript ??榛肿?/h1>

原文链接:The state of JavaScript modules

ESM, CJS, UMD, AMD?—?到底应该选择哪一个?

最近 在 twitter 上有很多关于 ES Module 现状的讨论,尤其是在 Node.js 上,他们计划引入新的文件扩展名 *.mjs。人们有足够理由对此感到 担忧和不确定,因为这个话题异常复杂,接下来会尽力阐述清楚问题。

来自远古的恐惧

大多数前端开发者应该还记得 Javascript 依赖管理的黑暗时期。那个时候,你需要把一个库复制粘贴到 vendor 文件夹,然后作为一个全局变量引入,要自己去按次序组合所有东西,可能还要管理命名空间。

在过去的那些年,我们能深刻体会到公共??楦袷交椭醒肽?楣芾淼募壑怠?/p>

在今天,不管是发布还是使用一个库都要容易得多,只需要使用 npm publishnpm install 命令就行。这就是人们会那么紧张两种??橄低臣嫒菪晕侍獾脑颍核遣幌胧ヒ延械氖媸是?。

接下来我会解释和总结现有实现的情况,以及为什么 Node 生态迁移到 ES Module(ESM)会那么难。在最后,总结这些变化对 webpack 使用者和??樽髡哂惺裁从跋臁?/p>

现有实现

目前,ESM 有三种方式的实现:

为了更好地理解现在的讨论,首先要知道 ES2015 包含两种模式:

  • script 用于具有全局命名空间的常规脚本
  • module 用于具有明确导入和导出的??榛?/li>

如果你试图在 script 标签使用 import 或者 export 语句,会抛出一个 SyntaxError。这种语句在全局环境下没有任何意义。另一方面,module 模式即意味着严格模式,禁止使用某些语言特性,比如 with 语句。因此,需要在脚本被解析和执行之前定义模式。

浏览器中的 ESM

截至到 2017 年 5 月,所有主流浏览器都开始做了 ESM 的实现工作。不过,大部分仍处于在实验性质。这里不会做详细介绍,因为 Jake Archibald 已经写了一篇很厉害的文章。

除了一些小的困难,在浏览器中实现起来非常容易,因为以前并没有??橄低?。想要指定 module 模式,需要在 script 标签添加 type="module" 属性,如下所示:

<script type="module" src="main.js"></script>

在一个模块中,现在只能使用有效的 URL 作为模块标识符。??楸晔斗怯糜?require 或 import 其他模块的字符串。为了确保未来兼容 CJS 模块标识符,“纯” 导入标识符(如 import "lodash")现在还不支持。??楸晔斗匦胧蔷?URL 或者是以 /, ./, ../ 开头:

// Supported:
import {foo} from 'https://jakearchibald.com/utils/bar.js';
import {foo} from '/utils/bar.js';
import {foo} from './bar.js';
import {foo} from '../bar.js';

// Not supported:
import {foo} from 'bar.js';
import {foo} from 'utils/bar.js';
// Example from https://jakearchibald.com/2017/es-modules-in-browsers/

同样需要注意的是,一旦处在一个??橹校扛龅既胍步唤馕鑫?module,而且没有办法 import 一个 script

ESM 与 webpack

类似 webpack 这样的构建工具通?;岢⑹杂?module 模式解析代码,有问题再切回到 script 模式。这些工具最终会生成一段 script,通常是在一定程度上模拟 CJS 和 ESM 行为的??樵诵惺薄?/p>

我们以这两个简单的 ESM 为例:

// a.js
export let number = 42;
export function incr() {
    number++;
}
// test.js
import { number } from "./a";

console.log(number);

webpack 使用函数包装器封装??榉段Ш投韵笠美茨D?ESM 实时绑定。每次编译,还包括一个模块运行时,负责引导和缓存模块。此外,将??楸晔蹲晃帜??ID。这样可以减少打包的大小和引导时间。

这是什么意思呢?我们来看看编译输出:

(function(modules) {
    // This is the module runtime.
    // It's only included once per compilation.
    // Other chunks share the same runtime.
    var installedModules = {};
    // The require function
    function __webpack_require__(moduleId) {
        ...
    }
    ...
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 1);
})
([ // An array that maps module ids to functions
    // a.js as module id 0
    function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        Object.defineProperty(__webpack_exports__, "a", {
            configurable: false,
            enumerable: true,
            get: () => number
        });

        let number = 42;

        function incr() {
            number++;
        }
    },
    // test.js as module id 1
    function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        var __WEBPACK_IMPORTED_MODULE_0__a__ = __webpack_require__(0);

        // Object reference as "live binding"
        console.log(__WEBPACK_IMPORTED_MODULE_0__a__["a" /* number */]);
    }
]);

简化的 webpack 输出,模拟 ES Modules 行为

结果已经简化并删除了一些与此示例无关的代码。你会发现,webpack 在 exports 对象上将所有 export 语句替换成 Object.defineProperty,并使用属性访问器替换对引入值的所有引用?;挂⒁饷扛?ESM 开始时的 "use strict" 指令,这是由 webpack 自动添加,在 ESM 中必须是严格模式。

这种实现只是模拟,因为它试图模仿 ESM 和 CJS 的行为 -- 但不是与其完全保持一致。比如,这种模拟并不符合某些边缘情况。看下面这个??椋?/p>

console.log(this);

如果你通过加上 babel-preset-es2015 的 Babel 来运行,结果是:

"use strict";
console.log(undefined);

从输出结果可以看出,Babel 假设默认是 ESM,因为 module 模式即代表严格模式,在严格模式下会将 this 初始化为 undefined。

然而,使用 webpack,结果是:

(function(module, exports) {

console.log(this);

})

在引导??槭?,this 将指向 exports ,与 Node.js 使用的 CJS 行为一致。这是因为语法上不确定是 script 还是 module,解析器无法判断该??槭?ESM 还是 CJS。在不明确的时候,webpack 会模拟 CJS,因为它仍然是最受欢迎的模块风格。

这种模拟其实已经包含了很多情况,因为模块作者通?;岜苊庹庵执?。然而,“很多情况”对于像 Node.js 这样的平台是不够的,因为它需要保证所有有效的 JavaScript 代码都能正常运行。

Node.js 中的 ESM

Node.js 在执行 ESM 时遇到了麻烦,因为仍然需要支持 CJS,语法看起来相似,但运行时行为完全不同。Node.js 核心技术委员会(CTC)成员 James M Snell 撰写了一篇很好的文章来解释 CJS 与 ESM 之间的差异。

归结起来,CJS 是一个动态??橄低?,ESM 是静态??橄低场?/p>

CJS

  • 允许动态同步 require()
  • 导出仅在??橹葱泻蟛胖?/li>
  • 导出可以在??槌跏蓟筇砑樱婊缓蜕境?/li>

ESM

  • 只允许静态同步 import
  • 在??橹葱兄埃既牒偷汲鲆丫亓?/li>
  • 导入和导出是不可变的

由于 CJS 早于 ES2015,所以一直在 script 模式下解析,封装通过使用函数包装器实现。在 Node.js 中加载 CJS,实际上会执行与此类似的代码:

const module = {
    exports: {}
};
const require = makeRequireFunction();
const filename = "...";
const dirname = "...";
(function (exports, require, module, __filename, __dirname) {
/* YOUR CODE */
})(module.exports, require, module, filename, dirname);

对 Node.js 的 CommonJS ??榈募虻ズ?/p>

问题出现了,将两个??橄低臣傻酵桓鲈诵惺笔保珽SM 和 CJS 之间的循环依赖可能会迅速导致类似死锁的情况。

而且,由于现有 CJS ??槭颗哟?,也不能直接放弃对 CJS 的支持。为了避免 Node.js 生态的中断,有两点已经很明显:

  • 现有的 CJS 代码必须以相同的方式继续工作
  • 两个??橄低扯急匦胪鼻揖】赡芪薹斓毓ぷ?/li>

目前的权衡

2017 年 3 月,经过几个月的讨论,CTC 终于找到了一种解决问题的办法。由于在 ES 规范和引擎不改变的情况下无法进行无缝集成,CTC 决定开始一些权衡之后的实现工作

1.ESM 必须是 *.mjs 文件扩展名

这是由于上面提及的模糊语法问题,无法通过解析来确切知晓 JavaScript 代码是什么类型。为了 Node.js 向后兼容的目标,作者需要加入一种新模式。已经有关于各种替代品的讨论,但使用不同文件扩展名是解决目前问题的最佳权衡。

2.CJS 只能异步导入 ESM import()

Node.js 将异步加载 ESM,以便尽可能接近浏览器的行为。因此,同步的 require() 在 ESM 是不可能的,并且依赖于 ESM 的每个功能都需要异步:

const driverPromise = import("dbdriver");

exports.readFromDb = async (query) => {
   return (await driverPromise).read(query);
};

3. CJS 向 ESM 暴露一个不可变的默认导出

使用 Babel 或 Webpack,我们通常将 CJS 重构为 ESM,如下所示:

// CJS
const { a, b } = require("c");
// ESM
import { a, b } from "c";

再一次地,他们的语法看起来很相似,但忽略了 CJS 中没有命名导出的事实。只有一个叫做 default 的导出,等同于在 CJS ??橥瓿杉扑愫笠桓霾豢杀涞?module.exports 。从技术上讲,有可能将 module.exports 解构成命名导入,但这需要对标准作更大的变更。这就是现在 CTC 决定采取这种方式的原因。

4.??榉段У谋淞坷嗨?modulerequire 以及 __filename 在 ESM 不存在

Node.js 和浏览器会实现一些 ESM 的特性,但标准化过程仍在进行中

鉴于将 CJS 和 ESM 集成到一个运行时的工程挑战,CTC 在评估边缘情况和权衡方面做了非常好的工作。比如使用不同的文件扩展名是就是一个很简单的解决方案。

实际上,一个文件扩展名可以认为是一个二进制文件如何解释的提示。如果一个 module 不是 script,我们应该使用不同的文件扩展名。其他工具(如 linter 或 IDE )也可以获取相同信息。

当然,引入新的文件扩展名有成本,但是一旦服务器和其他应用程序确认 *.mjs 为JavaScript,我们很快就会忘记这个争议。

将 * .mjs 作为 Node.js 的 Python 3?

考虑到所有这些限制,人们可能会问,这种过渡将对现在的生态造成什么样的损害。虽然 CTC 会努力解决问题,但社区如何采用这一点仍然存在很大不确定性。这种不确定性 被众多知名的 NPM 模块作者 再次强调,他们声称将不会在模块中使用 *.mjs。

Python 3 is killing Python

很难预测社区如何反应,但是应该不会对现在的生态造成大破坏,甚至能看到从 CJS 平稳过渡到 ESM。主要有两个原因:

1.与 CJS 严格向后兼容

那些不喜欢 ESM 的??樽髡呖梢约绦褂?CJS,保证自己不被排挤出局。这样他们自己的代码不会受到采用 ESM 的影响,降低迁移到另一个运行时的可能性,让 NPM 迁移到新生态变得容易。从 CJS 到 ESM 的重构给包维护者带来额外工作,不能指望所有人都有时间。

2. CJS 在 ESM 中的无缝整合

从 ESM 导入 CJS ??榉浅<虻?。需要注意的是,CJS 仅导出一个默认值。一旦处于 ESM,甚至可能根本不会注意到依赖关系使用的??榉绺瘢绕涫怯朐?CJS 中使用 await import()相比。

由于 ESM 的这个优点以及其他有点,比如开箱即用的 tree shaking 和浏览器兼容性,预计在未来几年内,我们可以看到向 ESM 的缓慢而稳定的过渡。CJS 的特性,如动态 require() 和猴子补丁导出,在 Node.js 社区一直是有争议的,不比 ESM 带来的好处。

这些对我来说意味着什么?

因为最近这些事情,很容易对目前存在的所有选择和限制感到困惑。在接下来,整理了开发人员面临的典型问题以及我们的回答:

现在需要重构现有的代码吗?

不需要。Node.js 才刚刚开始实现 ESM,仍然有大量的工作要做。James M Snell 预计至少还需要一年时间,还有很多变化的余地,所以现在重构是不安全的。

应该在新代码中使用 ESM 吗?

  • 如果你已经有或者打算使用像 webpack 这样的构建工具,答案是肯定的。这将更容易完成代码库的过渡,并使 tree shaking 成为可能。但要小心:一旦 Node.js 支持原生 ESM,可能需要重构其中的一些部分。
  • 如果你正在编写一个库,答案是也肯定的,你的??槭褂谜呓芤嬗?tree shaking。
  • 如果你不想进行构建操作,或者正在编写一个 Node.js 应用程序,还是用 CJS 吧。

现在应该使用 .mjs 吗?

不要这样做,目前没有什么好处,工具支持依然薄弱。建议一旦原生 ESM 支持登陆 Node.js,尽快开始迁移。记住,浏览器只关心 MIME 类型,而不是文件扩展名。

应该关心浏览器兼容性吗?

是的,需要在一定程度上关注这个问题。 不应该在导入语句中省略 .js 扩展名,因为浏览器需要完整的 URL,无法像 Node.js 这样执行路径查询。同样,应该避免 index.js 文件。不过,人们并不会很快在浏览器中使用 NPM 软件包,因为仍然不能 bare 导入。

作为库作者该怎么办?

用 ESM 编写代码,并使用 Rollup 或 Webpack 转换成单个 CJS ??椋缓笤?package.jsonmain 字段指向此 CJS 包,并将 module 字段指向原始 ESM。如果还使用 ESM 之外的其他新语言功能,则应编译成 ES5,并提供 CJS 和 ESM 的打包。这样,你的库用户仍然可以从 tree shaking 获利而无需对代码进行转换。

看一下这些完成 tree shaking 的???/p>

总结

关于 ES ??橛泻芏嗖蝗范ㄐ浴S捎谀壳?Node.js 在实现上的权衡,开发人员担心可能会破坏 Node.js 的生态。

这些还不会发生,有两个原因:CJS 的严格的后向兼容和 CJS 在 ESM 中的无缝集成

在 Node.js 发布原生 ESM 支持之前,应该仍然使用 Rollup 和 Webpack 等工具。它们在一定程度上模拟了 ESM 环境,但要注意它们不完全符合规范。此外,使用打包仍然是个很好的选择,一旦可以在浏览器中使用 NPM 软件包。

我们 webpack 团队正在努力做一些工作,帮助开发者平稳过渡。为了这个目标,我们计划在 Node.js 的 ESM 支持成熟后,模拟 Node.js 导入 CJS 的方式。

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者

  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容

  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,154评论 7 35
  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,685评论 7 110
  • 版权声明:本文为博主原创文章,未经博主允许不得转载。 webpack介绍和使用 一、webpack介绍 1、由来 ...
    it筱竹阅读 11,089评论 0 21
  • 写在开头 先说说为什么要写这篇文章, 最初的原因是组里的小朋友们看了webpack文档后, 表情都是这样的: (摘...
    Lefter阅读 5,283评论 4 31
  • 今天是我第二次造访少年宫的中心书城,上一次是刚到深圳那会儿,因为面试的缘故,意外的走进了中心书城。第一次的初...
    東孫飛阅读 367评论 0 3