爲什麼 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。

參考資料

相關文章
相關標籤/搜索