博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
为何 ES Module 如此姗姗来迟
阅读量:6470 次
发布时间:2019-06-23

本文共 5580 字,大约阅读时间需要 18 分钟。

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

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

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

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

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

Module 的特性

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

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

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

解析器的难题

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

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

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

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

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

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

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

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

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

区分 JavaScript 文件类型的任务没法放在解析器里自动完成,我们需要在解析文件之前就知道它的类型。

浏览器的办法

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

当浏览器开始加载这个 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": ["."],}

这还没完,更多方案就不详述了,大家可以到 上查看。

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

现在的进展(2016.04.15)

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

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

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

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

关注 ES Module 的进展,还可以看看几个地方:

  1. Node 社区提案和讨论:
  2. V8 的实现:
  3. Blink 的实现:

愿 ES Module 早日到来。

Updates

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

  • 2016.01.08
    bmeck 提出关于 (增加新后缀.mjs),社区讨论开始。
  • 2016.02.06
    社区提的方案归纳起来,有。
  • 2016.04.15
    本文发布的日子。
  • 2016.04.20
    经过两个月的密集讨论,四个方向只剩下两个存活:.mjs 派和 package.json 派,然而这两派的争论非常激烈。
  • 2016.04.27
    鉴于 .mjs 已经在正式提案中,倘若讨论持续僵持不下,不出意外 .mjs 将会随着时间推移而正式成为规范。怀着这样的危机感,package.json 派发起了 来抗衡 .mjs 的提案,要求保持 .js 后缀不变而使用 package.json 来识别 ES Module。
  • 2016.06.14
    重大转折!bmeck 提出一个新的方案 :既然两边的纠结都是因为无法从文件本身识别 ES Module 而起,不妨调整一点语法细节(ES Module 中的 exports 语句不再是可选的,至少有一句 exports {} 来表明该文件是个 ES Module),两派的争论就这么迎刃而解了!
  • 2016.07.06
    经过 Node.js TSC 的讨论,Unambiguous JavaScript Grammar 方案。
  • 2016.07.07
    虽然 Unambiguous JavaScript Grammar ,但是考虑到距离 TC39 的七月会议只剩下一周时间,而 Node.js 这边希望做更充分的调研和测试再进行讨论,所以。
  • 2016.09.06
    Domenic 提了 作为动态加载的方案,有望取代 System.import()System.loader.import()
  • 2016.09.17
    ES Module 再次,相关的还有和 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 已经实现了 .mjs 加载器的原型,在 Node.js v9 中可以用 flag 的方式启用,(希望)在 v10 中正式推出。也就是还有一年的时间,一切顺利的话 2018 年 4 月就能看到 ES Module 正式加入 Node.js LTS。
  • 2017.05.11

    工具链对 .mjs 后缀的支持都在推进中:

    • Babel:
    • Babili/babel-minify already supports .mjs:
    • AVA:
    • Visual Studio Code:
  • 2018.03.30
    Node.js 项目中
  • 2018.04.25
    Node.js 10.0.0 发布,加入了对 ES Module 的实验性支持(需要 --experimental-modules 开启)
  • 2019.03.28
    新版 ES Module 设计定案,PR 合进主干(),特性有变,仍然使用 --experimental-modules 开启。目前的计划是赶上 4 月份 Node.js 12 发布,最终在 2019 年 10 月进入 LTS。

参考资料

转载地址:http://kujko.baihongyu.com/

你可能感兴趣的文章
SHOW CREATE DATABASE Syntax
查看>>
rsync常见问题及解决办法
查看>>
AKM项目轶事之GBS同事转入GDC
查看>>
MySQL日期 专题
查看>>
C#中禁止程序多开
查看>>
分布式缓存Redis使用以及原理
查看>>
[LeetCode] Number of 1 Bits 位操作
查看>>
练习二:结对练习
查看>>
JSON中JObject和JArray,JValue序列化(Linq)
查看>>
杂七杂八
查看>>
Activity竟然有两个onCreate方法,可别用错了
查看>>
Linux经常使用命令(十六) - whereis
查看>>
Tomcat
查看>>
插件编译 版本问题
查看>>
android中TextView的阴影设置
查看>>
core dump相关
查看>>
MySQL如何导出带日期格式的文件
查看>>
Linux五种IO模型
查看>>
Bootstrap技术: 模式对话框的使用
查看>>
小知识,用myeclipes找jar
查看>>