为什么 ES Module 如此姗姗来迟

说明:本文发布以后,此问题的推动峰回路转,不停有新内容。文末新增一节 Updates,跟进本文发布以后的 ES Module 标准化进展状况。javascript

浏览器大战多年了热度依旧高涨,你们终于在 JS 新特性的部署上达成一致纷纷追赶最新标准,然而 ES2015 中的 ES Module 这个万众期待的重要特性却始终迟迟未能实现。php

等 2020 年回望历史,假若咱们错过了 ES Module 这艘船而 Node.js 死在汪洋大海之中,没有任何其余技术问题的重要性能够与此相比。
-- issac

Module 的规范是完工了的,只是对于模块如何加载和解析留给了“实现环境决定”——按历史经验,问题每每就出如今这一环。固然了不是烫手山芋 W3C 也不会就这么轻松甩开对吧,事实上这也不是 W3C 一家的事情,牵涉到 TC3九、Node 技术委员会、Node 和前端两个开发社群,以及 npm 公司。html

故事很长,咱们从头提及。importexport 的语法规范很明确,模块的解析器 V8 早已实现,万事俱备只欠加载。区区加载能有多麻烦?前端

Module 的特性

在新规范下,JavaScript 程序划分红两种类型:脚本(咱们之前写的传统JS)和模块(ES规范中新定义的 Module),模块有四项于脚本不一样的特性:java

  1. 强制严格模式(没法取消)
  2. 执行环境在一个非全局的做用域中
  3. 可使用 import 导入其余 Module 的 binding
  4. 可使用 export 导出本 Module 的 binding

看上去规则简单明白,可是要让一个解析器(parser)区分兼容这两种模式还挺复杂的。node

解析器的难题

看看代码中是否包含 importexport 关键字不就能够判断它的类型了么?

不行。首先猜想用户意图是个危险行为,若是你猜对了,就更加掩盖了猜错可能会形成的风险。git

而严格模式,除了运行时的一些要求以外还定义了几个语法错误:es6

  1. 使用 with 关键字;
  2. 使用八进制字面量(如 010);
  3. 函数参数重名;
  4. 对象属性重名(仅在 ES5 环境。ES6 取消了此错误);
  5. 使用 implementsinterfaceletpackageprivateprotectedpublicstaticyield 做为标识符。

这些语法错误须要在解析时就抛出来。因此若是以脚本模式解析到了文件末尾才发现有 export,就得从头从新解析整个文件来捕捉上述语法错误。github

那咱们换一条路,开始先假定为模块进行解析代码。既然 Module 语法至关于严格模式 + 导入导出 (importexport),咱们能够用脚本模式 + 导入导出的语法来解析整个文件。然而这种解析规则已经超越了规范定义,这么扭曲的路线能够预见它成为 Bug 源泉的样子。npm

危险但不是不可能。OK 真正的麻烦来了:按照规范 importexport 都是可选的——你能够写一个 Module,既不导入也不导出任何东西,它只是对全局做用域作些小动做,好比这样:

// 一个合法的 Module
window.addEventListener("load", function() {
    console.log("Window is loaded");
});
// WAT!

总的来讲,包含 importexport 代表它必定是个 Module,但没有这两个关键字却不能证实它不是 Module。 ╮(╯_╰)╭

区分 JavaScript 文件类型的任务无法放在解析器里自动完成,咱们须要在解析文件以前就知道它的类型。

浏览器的办法

这就是为何浏览器的模块引用是这个写法:

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

当浏览器开始加载这个 foo.js,它会边加载边解析,碰到 import { bar } from './bar.js' 的第一时间开始加载依赖的 bar.js,加载完以后对其解析,检查其中是否导出了 bar。如此往复完成整个 Module 的解析。

Node.js 呢

到了 Node.js,新的问题来了。

做为世界上最大的软件包仓库,npm 中现有的软件包都是 CommonJS 规范。ES Module 须要可以与 CommonJS 模块共存,容许开发者们逐步转向新的语法。

所谓的共存,主要是指 import { foobar } from 'foobar' 语法要支持 CJS Module 和 ES Module 两种包格式——若是 import 只能用来导入 ES Module 而 require 能够导入任意模块,那么全部人都会用 require;若是 importrequire 各自负责导入各自的格式,那么开发者就须要知道全部依赖的库的格式,使用相应语法来导入它,而且在依赖的库们更换到新格式的时候修改本身的代码去兼容……在可预见的 CommonJS -> ES Module 漫长过渡期里这样的负担对社区而言不可接受。

为此社区提出了很多方案,(好消息)通过大量的讨论以后如今已经集中到两个选择还在讨论:

  1. 解析器自动检测。最大的好处是对用户而言透明,惋惜缘由如前所述,此方案已否认。
  2. 使用 "use module" 标注。一想到 JS 的将来永远都要在文件开头贴这么个膏药你们就不能忍了。否认。
  3. 新的文件后缀 .jsm。主要问题是现有社区工具链所有须要更新才能支持,另外和浏览器实现的统一也要考虑。
  4. package.json 上发挥。这个门类下的提议就更多了,好比添加一个 module 字段逐步替代掉 main
{
    // ...
    "module": "lib/index.js",
    "main": "old/index.js",
    // ...
}

这个方案只适用单入口的状况,对多文件(好比 require('foo/bar.js')的场景)就不行了。那就改为 modules 字段(复杂度陡升):

{
    // ...
    // files:
    "modules": ["lib/hello.js", "bin/hello.js"],

    // directories:
    "modules": ["lib", "bin"],

    // files and directories:
    "modules": ["lib", "bin", "special.js"],

    // if package never uses CJS Modules
    "modules": ["."],
}

这还没完,更多方案就不详述了,你们能够到 Node.js Wiki 上查看。

就我的偏好而言,尽管全部的方案都有利有弊,而 package.json 这条路为了兼容各类需求,修改版的提案已经愈来愈复杂,比较起来 .jsm 后缀却是愈发显得简单清晰了。我更喜欢这个干净的解决方案。

如今的进展(2016.04.15)

<script type="module" /> 已经加入 HTML 规范,WhatWG 刚刚发了一篇文章讲述他们如何通过坚苦卓绝的努力达成这一目标,接下来就看浏览器厂商实现了。

除此以外 WhatWG 手上还有一个 ES Module loader 规范,用于指定 Module 的动态加载方式。它曾经是 ES6 草案的一部分,但由于 ES2015 “要赶着发布来不及了”不幸被砍,目前归属 WhatWG 推动

Node.js 这边,在至关一段时间里咱们还要借助 transpiler 来体验 ES Module。这件事须要 V八、Node.js、WhatWG 共同协调完成。

按计划本月 Node.js 发布 6.0,顺利的话能够 肯定集成 V8 5.0(BTW,一天后 V8 发布了 5.1),对 ES2015 的特性支持达到 93%——看来 ES Module 极可能会成为 “The last ES2015 feature” 了。

关注 ES Module 的进展,还能够看看几个地方:

  1. Node 社区提案和讨论:https://github.com/nodejs/nod...
  2. V8 的实现:https://bugs.chromium.org/p/v...
  3. Blink 的实现:https://bugs.chromium.org/p/c...

愿 ES Module 早日到来。

Updates

关于 ES Module 在 Node.js 环境下的识别方案,从一月份 bmeck 提出提案开始社区就持续沟通和争论,如下是相关进展更新。

  • 2016.01.08
    bmeck 提出关于 ES Module 的提案(增长新后缀.mjs),社区讨论开始。
  • 2016.02.06
    社区提的方案概括起来,有四个方向
  • 2016.04.15
    本文发布的日子。
  • 2016.04.20
    通过两个月的密集讨论,四个方向只剩下两个存活:.mjs 派和 package.json 派,然而这两派的争论很是激烈。
  • 2016.04.27
    鉴于 .mjs 已经在正式提案中,假若讨论持续僵持不下,不出意外 .mjs 将会随着时间推移而正式成为规范。怀着这样的危机感,package.json 派发起了 In defense of dot js 来抗衡 .mjs 的提案,要求保持 .js 后缀不变而使用 package.json 来识别 ES Module。
  • 2016.06.14
    重大转折!bmeck 提出一个新的方案 UnambiguousJavaScriptGrammar:既然两边的纠结都是由于没法从文件自己识别 ES Module 而起,不妨调整一点语法细节(ES Module 中的 exports 语句再也不是可选的,至少有一句 exports {} 来代表该文件是个 ES Module),两派的争论就这么迎刃而解了!
  • 2016.07.06
    通过 Node.js TSC 的讨论,Unambiguous JavaScript Grammar 方案正式加入提议(proposal)
  • 2016.07.07
    虽然 Unambiguous JavaScript Grammar 加入了 Node.js 的草案提案(5.1章),可是考虑到距离 TC39 的七月会议只剩下一周时间,而 Node.js 这边但愿作更充分的调研和测试再进行讨论,因此从此次 TC39 的议程中拿掉了
  • 2016.09.06
    Domenic 提了 import() 做为动态加载的方案,有望取代 System.import()System.loader.import()
  • 2016.09.17
    ES Module 再次提上 TC39 的议事日程,相关的还有内建模块import()
  • 2016.09.30
    TC39 9月碰头会的与会者纷纷表示此次会议进展使人愉快,会议内容汇总在此,以及一些补充

    • Node.js 开发者想要提出一些修改规范的建议,也不知道合适不合适,沟通以后发现 TC39 是很是关心和在乎每一个社区的需求的(你们相谈甚欢)。
    • 本来的 ES 规范要求模块加载过程须要先完成静态 parse 而后再 evaluate,可是如今的 Node.js CommonJS 模块没法知足这个要求(CJS 模块必须 evaluate 以后才知道 exports 的是什么)。讨论下来规范将会改成容许 parse 过程在碰到 import CJS 模块时进入一个挂起的状态,等待依赖树中的 CJS 模块 evaluate 以后再完成 parse。
    • 对模块类型的检测目前是三个方案选项:

      1. Unambiguous JavaScript Grammar 看上去比较简单,但实现起来仍是有很多坑;
      2. package.json 的办法比较累赘,局限也多;
      3. .mjs 的方案最简单,看来是最可行的,并且也跟 Node.js 现有方式一致(用后缀 .node.json.js来区分加载类型)。除非 Unambiguous JavaScript Grammar 的实现问题都解决掉,不然最终方案就是它了。
    • import() 你们都以为没问题,稳步推动中。
    • 因为 ES Module 的静态特性,之前给 CJS 模块作动态 Mock、MonkeyPatch 的方式都不行了。不过解决办法也有,一是在加载阶段提供钩子,二是容许对已经加载的模块作热替换。
  • 2017.02.12
    Node.js CTC 和 TC39 的讨论:

    • 因为 ES6 模块的异步特性,require() 将没法加载 ES6 模块。
    • Babel 目前支持的 import { foo } from 'node-cjs-module' 也不符合规范,想 import 一个 NCJS 模块的话只能 import m from 'node-cjs-module' 而后 m.foo() 调用。
    • .mjs 是问题最少的选择。
    • (悲伤的消息来了)就目前剩余的工做内容估计,距离 ES6 Module 最终实现大约还有至少一年的时间(往好的一面想,终于看获得 timeline 了)。
  • 2017.05.10
    bmeck 在 Twitter 表示已经实现了 .mjs 加载器的原型,在 Node.js v9 中能够用 flag 的方式启用,(但愿)在 v10 中正式推出。也就是还有一年的时间,一切顺利的话 2018 年 4 月就能看到 ES Module 正式加入 Node.js LTS。
  • 2017.05.11
    工具链对 .mjs 后缀的支持都在推动中:

  • 2018.03.30
    Node.js 项目中和 ES Module 实现相关的 Issue 和 PR
  • 2018.04.25
    Node.js 10.0.0 发布,加入了对 ES Module 的实验性支持(须要 --experimental-modules 开启)
    https://github.com/nodejs/nod...
  • 2019.03.28
    新版 ES Module 设计定案,PR 合进主干(https://github.com/nodejs/nod...),特性有变,仍然使用 --experimental-modules 开启。目前的计划是遇上 4 月份 Node.js 12 发布,最终在 2019 年 10 月进入 LTS。

参考资料

相关文章
相关标签/搜索