当今世代浏览器大战风起云涌,大家争先恐后地部署 ES2015 新特性,然 ES Module 这个万众期待的重要特性却始终迟迟未能实现。Module 的规范是完工了的,只是对于模块如何加载和解析留给了“实现环境决定”——根据历史经验这部分总是会出问题,不是烫手山芋 W3C 也不会就这么轻松甩开对不。
import
和 export
的语法规范很明确,模块的解析器 V8 早已实现,万事俱备只欠加载。区区加载能有多麻烦?
在新规范下,JavaScript 程序划分成两种类型:脚本(我们以前写的传统JS)和模块(ES规范中新定义的 Module),模块有四项于脚本不同的特性:
强制严格模式(无法取消)
执行环境在一个非全局的作用域中
可以使用 import
导入其他 Module 的 binding
可以使用 export
导出本 Module 的 binding
看起来不算什么大事情,但是要让一个解析器(parser)兼容这两种模式还挺复杂的。
看看代码中是否包含 import
和 export
关键字不就可以判断它的类型了么?
不行。首先猜测用户意图是个危险行为,如果你猜对了,就更加掩盖了猜错可能会造成的风险。
而严格模式,除了运行时的一些要求之外还定义了几个语法错误:
使用 with
关键字
函数参数重名
十进制字面量(如 010
)
重复的属性名(仅在 ES5 环境。ES6 取消了此错误)
使用 implements
、 interface
、 let
、 package
、 private
、 protected
、 public
、 static
或 yield
作为标识符。
这些语法错误需要在解析时就抛出来。所以如果以脚本模式解析到了文件末尾才发现有 export
,就得从头重新解析一遍整个文件来捕捉上面那些语法错误。
那我们换一条路,开始就以模块的思路解析代码。既然 Module 语法相当于严格模式 + 导入导出,要同时支持导入导出和脚本模式的语法解析都不报错,我们可以用脚本模式 + 导入导出来解析整个文件。然而这种解析规则已经超越了规范定义,这么扭曲的路线可以预见它成为 Bug 源泉的样子。
蛋疼但不是不可能。OK 真正的麻烦来了: import
和 export
都是可选的——你可以写一个 Module,既不导入也不导出任何东西,它只是对全局作用域做些小动作,比如这样:
// 一个合法的 Module window.addEventListener("load", function() { console.log("Window is loaded"); }); // WAT!
总的来说,包含 import
或 export
表明它一定是个 Module,但没有这两个关键字却不能证明它不是 Module。 ╮(╯_╰)╭
区分 JavaScript 文件类型的任务没法放在解析器里自动完成,我们需要在解析文件之前就知道它的类型。
这就是为什么浏览器的模块引用是这个写法:
<script type="module" src="foo.js"></script>
当浏览器开始加载这个 foo.js
,它会边加载边解析,碰到 import { bar } from './bar.js'
的第一时间开始加载依赖的 bar.js
,加载完之后对其解析,检查其中是否导出了 bar
。如此往复完成整个 Module 的解析。
到了 Node.js,新的问题来了。
作为 世界上最大的 软件包仓库,npm 中现有的软件包都是 CommonJS 规范。ES Module 需要能够与 CommonJS 模块共存,允许开发者们逐步转向新的语法。
所谓的共存,主要是指 import { foobar } from 'foobar'
语法要支持 CJS Module 和 ES Module 两种包格式——如果 import
只能用来导入 ES Module 而 require
可以导入任意模块,那么所有人都会用 require
;如果 import
和 require
各自负责导入各自的格式,那么开发者就需要知道所有依赖的库的格式,使用相应语法来导入它,并且在依赖们更换到新格式的时候修改自己的代码去兼容,在可预见的漫长过渡期里这样的负担对社区而言不可接受。
为此社区提出了不少方案,(好消息)经过大量的讨论之后现在已经集中到两个选择还在讨论:
解析器自动检测。最大的好处是对用户而言透明,可惜原因如前所述,此方案已否定。
使用 "use module"
标注 。一想到 JS 的未来永远都要在文件开头贴这么个膏药大家就不能忍了。否定。
新的文件后缀 .jsm
。主要问题是现有社区工具链全部需要更新才能支持,另外和浏览器实现的统一也要考虑。
在 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 的进展,可以到这几个地方:
Node 社区提案和讨论: https://github.com/nodejs/node-eps/pull/...
V8 的实现: https://bugs.chromium.org/p/v8/issues/de...
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...