转载

为何 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. 函数参数重名

  3. 十进制字面量(如 010

  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 各自负责导入各自的格式,那么开发者就需要知道所有依赖的库的格式,使用相应语法来导入它,并且在依赖们更换到新格式的时候修改自己的代码去兼容,在可预见的漫长过渡期里这样的负担对社区而言不可接受。

为此社区提出了不少方案,(好消息)经过大量的讨论之后现在已经集中到两个选择还在讨论:

  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 规范 。

Node.js 这边在相当一段时间里我们还要借助 transpiler 来体验 ES Module。这件事需要 V8、Node.js、WhatWG 共同协调完成。

WhatWG 手上除了 HTML 规范的部分(已经完成),还有一个 ES Module loader 规范,用于指定 Module 的动态加载方式,曾经是 ES6 草案的一部分 ,但因为 ES2015 “要赶着发布来不及了”不幸被砍,目前由 WhatWG 推进: https://github.com/ModuleLoader/es6-modu... 。

按计划本月 Node.js 会发布 6.0,顺利的话可以 集成 V8 5.0 ,届时对 ES2015 的特性支持将能 达到 93% ——看来 ES Module 很可能会成为 “The last ES2015 feature” 了。

关注 ES Module 的进展,可以到这几个地方:

  1. Node 社区提案和讨论: https://github.com/nodejs/node-eps/pull/...

  2. V8 的实现: https://bugs.chromium.org/p/v8/issues/de...

  3. Blink 的实现: https://bugs.chromium.org/p/chromium/iss...

愿 ES Module 早日到来。

参考资料

  • https://www.nczonline.net/blog/2016/04/e...

  • https://github.com/nodejs/node/wiki/ES6-...

  • http://awal.js.org/blog/es6/2015/09/10/s...

原文  https://segmentfault.com/a/1190000004940294
正文到此结束
Loading...