为何 ES Module 如此姗姗来迟

521 查看

本文发布之后,社区的争议推进可谓峰回路转,文末新增一节 Updates,跟进本文发布之后的 ES Module 标准化进展情况。

浏览器大战风起云涌,大家争先恐后地部署 ES2015 新特性,然 ES Module 这个万众期待的重要特性却始终迟迟未能实现。Module 的规范是完工了的,只是对于模块如何加载和解析留给了“实现环境决定”——根据历史经验问题往往就出现在“实现环境决定”这一环。当然了不是烫手山芋 W3C 也不会就这么轻松甩开对吧。

importexport 的语法规范很明确,模块的解析器 V8 早已实现,万事俱备只欠加载。区区加载能有多麻烦?

Module 的特性

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

  1. 强制严格模式(无法取消)

  2. 执行环境在一个非全局的作用域中

  3. 可以使用 import 导入其他 Module 的 binding

  4. 可以使用 export 导出本 Module 的 binding

看起来不算什么大事情,但是要让一个解析器(parser)兼容这两种模式还挺复杂的。

解析器的难题

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

不行。首先猜测用户意图是个危险行为,如果你猜对了,就更加掩盖了猜错可能会造成的风险。

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

  1. 使用 with 关键字;

  2. 使用八进制字面量(如 010);

  3. 函数参数重名;

  4. 对象属性重名(仅在 ES5 环境。ES6 取消了此错误);

  5. 使用 implementsinterfaceletpackageprivateprotectedpublicstaticyield 作为标识符。

这些语法错误需要在解析时就抛出来。所以如果以脚本模式解析到了文件末尾才发现有 export,就得从头重新解析整个文件来捕捉上述语法错误。

那我们换一条路,开始就以模块的思路解析代码。既然 Module 语法相当于严格模式 + 导入导出,要同时支持导入导出和脚本模式的语法解析都不报错,我们可以用脚本模式 + 导入导出来解析整个文件。然而这种解析规则已经超越了规范定义,这么扭曲的路线可以预见它成为 Bug 源泉的样子。

蛋疼但不是不可能。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 后缀倒是愈发显得简单清晰了。我更喜欢这个干净的解决方案。

现在的进展

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

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

Node.js 这边,在相当一段时间里我们还要借助 transpiler 来体验 ES Module。这件事需要 V8、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)

参考资料