Loader

Loader 是 ECMAScript 定義要來處理 module import/export 等等事情的底層介面,ES6 的 module 我一直都很好奇,到底要怎麼去找 import 進來模組的原始碼,會好奇這點是因為如果是網頁環境,所有其它模組的原始碼一定是在遠端的 server 上,要拿到勢必是一個 request,然後還要等下載,總之就是非同步的流程,以前在 ES5 的話,要做非同步控制大概就是要做成 callback 的形式,所以會有像 AMD 那樣的設計出來,加上 module 名稱和檔案名稱可能又有差別,像是差個副檔名之類的,而 ES6 提供了原生的 import/export 語法來定義 module,所以我就很好奇它的底層要怎樣設計才能標準化。

Addy Osmani 有建立一個 Loader 的 polyfill 給 ES5 環境使用 Loader API,就叫做 ES6 Module Loader Polyfill,這個 polyfill 內部實做是照當初 ES6 draft 來寫的,其實還蠻複雜,不過把讀取一個 module 的事情拆分一下,可以分成以下幾個步驟:

  1. Normalize:根據給的名稱取得實際的 module name
  2. Locate:根據實際的 module name 取得 module 的位置
  3. Fetch:根據 module 位置去取得檔案內容
  4. Translate:如果有需要對檔案內容作任何修改,就在這裡處理
  5. Instaniate:最後是根據檔案內容(程式碼),判斷有哪些相依模組和知道如何初始化這個模組

以上幾個步驟是屬於 Loader 可自訂化的部分,到 instaniate 完成後,Loader 就繼續去讀其它相依的模組,相依模組都準備好之後,就可以使用模組的的程式碼,正式的把模組建立起來,ES spec 細部還定義了很多實做細節,像是非同步的操作都是用 Promise 來做流程控制,還有 Loader 也有個 module 的 registry 可以來保存已經讀好的模組,就不用一直重新建立,另外還有一些內部溝通的資料結構,像 instaniate 步驟要回傳一個物件,裡面有兩個屬性分別是depsexecutedeps是相依模組名稱的陣列,execute則是該模組本身的初始化函數,參數的數量要剛好和相依模組數量一樣,回傳的則是 Module 物件等等。

其實目前的 ES6 spec draft 已經把 Loader 拿掉了,TC39 決定把他獨立出來,目前是 WHATWG 接手繼續,最新的版本已經改很大了,看起來感覺有簡化不少,不過上面的五個步驟基本上還是存在,我一開始看到這五個步驟加上說可以自訂還沒什麼感覺,只是覺得奇怪為什麼細節沒寫,沒錯,這五個步驟在 spec draft 都只有介紹目的,不像其它的操作有詳細的寫出 method 內的流程,關鍵的地方就在於 JavaScript 已經不是單純只是在瀏覽器上跑的語言了,把這部分抽象化就是為了讓它可以同時在瀏覽器環境和單機環境下都可以實做,根據不同的 JS 環境去實做相對應的步驟細節,像是fetch在瀏覽器下就是真的用 fetch 去拉檔案,但是在 node 下就變成讀檔案,而在 ES6 Module Loader Polyfill 下,就有實做一組瀏覽器環境下的操作,不過這組操作的實做未來也不會真的進到瀏覽器內,最大的問題在於第五個步驟的 instaniate。

Instaniate 這個步驟是要實做 ES6 module 一個很關鍵的部分,關鍵之處在於要把 module 的 dependency 找出來,在 ES6 module 裡面,有一個限制是每個 module 都必須要獨立一個檔案,所以不能一個檔案定義兩個 module,然後假設瀏覽器已經支援 ES6 module 了,只要 parse 程式碼成 AST 找出裡面的import就可以把 dependency 列出來。不過現在是 polyfill,瀏覽器也還不支援import,所以要實做 instaniate 自然需要能處理這個問題,ES6 Module Loader Polyfill 的作法是使用 transpiler,目前支援 TraceurBabel,把本來用importexport寫的模組轉成用類似的 AMD 模組定義的型式,而這邊用的型式是 systemjs 裡面提供的 System.register,這個方法本身並不是 ES spec 裡面定義的,比較像是為了處理這尷尬時間點所設計的替代方案。

本來我是想看看,是不是能夠只靠 Loader 就把 ES6 的 module 機制在現在的瀏覽器上建立起來,結果發現只靠 ES6 Module Loader Polyfill 是辦不到的,Loader API 並沒有定義模組的語法,如果用 ES6 的語法來定義模組還需要 transpiler 來從程式碼中分析出 module dependency,不過我不太想要把整包 transpiler 也放到 translate 裡面用,雖然可以自己寫一個什麼事情都不做的 translate function,但是要解決 dependency 的話還是會需要像System.register的幫助,總之到這邊,可以發現一個重點是,Loader 不管 module 定義的方法,雖然假想情境下是用 ES6 module 語法,每個檔案代表一個 module,然後用importexport來定義相依模組和提供的 method,不過其實 Loader 也是可以處理 AMD、CommonJS 甚至是 NodeJS 型式的模組定義,只是要有人去實做這部分的 translate 和 instantiate 的部分,而 SystemJS最新版(0.16)就是這樣一個專案,它號稱是 universal module loader,支援 AMD、CommonJS、NodeJS 和 ES6 的模組定義,然後在最新的版本,改成使用 ES6 Module Loader Polyfill 的機制來做 module 的讀取、相依性的判斷和模組初始化,雖然有些地方沒有真的照之前的 spec 來實做。

另外一個之前不太清楚的問題也藉此搞清楚了,ES6 module 有限制一個檔案定義一個 module,而現在的 web application 常常為了效能問題,都把多個檔案合併成一個檔案,這時就不能用 ES6 module 了,當然也可以用像現在 SystemJS 的作法來處理,不過其實再過一兩年 HTTP/2 普及後,也不需要這樣搞了,會變成只需要 minimize,這部分倒是還可以接受。

ES6 裡面的 Loader 本身其實是一個 constructor,放在另外一個 ES6 提供的新東西Reflect下面,而用Reflect.Loader建立 Loader instance 時可以順便給他一些參數,像是前面提到的讀模組的五個步驟的實做,或是領域(realm),而System物件則是該 JS 環境下的預設的 Loader,理論上如果是瀏覽器環境,它就會知道怎樣去 fetch 遠端的程式碼回來,如果是 NodeJS 就會改用 file system 讀檔案,而且也知道要把模組放到那個 realm(理論上不同 frame 就是不同 realm),這樣大部分的使用都可以用System就好了,只有很少數的情形需要自己建立 Loader。其實上面說的 Loader API 不知道為什麼是移到 WHATWG 之後,幾乎是重新開始編寫,完整度欠佳,有些章節還是空的,另外也沒有定義System或是其它的新的替代方案,所以現在想要看看到底 Loader 內部怎麼做的話,要看舊版的 spec,可以去抓 2014 年 8 月的 ES6 draft rev 27 然後看看 CH 26, 15,對照 ES6 Module Loader Polyfill 的程式碼可能比好懂。