JS 中的模塊化

來源:止水的公開課《webpack 原理與實戰》,手碼整理的筆記(只有前半部分,後半部分關於 webpack 打包的內容由於視頻中的文檔沒有全部出現所以無法記錄)。

JS 中的模塊化

要明白我們的打包工具究竟做了什麼,首先必須明白的一點就是 JS 中的模塊化。在 ES6 規範之前,我們有 CommonJS、AMD的主流的模塊化規範。

CommonJS

Node.js 是一個基於 V8 引擎、事件驅動 I/O 的服務端 JS 運行環境,在2009年剛推出時,它就實現了一套名爲 CommonJS 的模塊化規範。
在 CommonJS 規範裏,每個 JS 文件就是一個模塊(Module),每個模塊內部可以使用 require 函數和 module.exports 對象來對模塊進行導入和導出。

// index.js
require('./moduleA');
var m = require('./moduleB');
console.log(m);

// moduleA.js
var m = require('./moduleB');
setTimeout(() => console.log(m), 1000);

// moduleB.js
var m = new Date().getTime();
module.exports = m;
  • index.js 代表的模塊通過執行 require 函數,分別加載了相對路徑爲 ./moduleA./moduleB 的兩個模塊,同時輸出 moduleB 模塊的結果。
  • moduleA.js 文件內也通過 require 函數加載了 moduleB.js 模塊,在 1s 後也輸出了加載進來的結果。
  • moduleB.js 文件內部相對來說就簡單的多,僅僅定義了一個時間戳,然後直接通過 module.exports 導出。

AMD

另一個爲 WEB 開發者所熟知的 JS 運行環境就是瀏覽器了。瀏覽器並沒有提供像 Node.js 裏一樣的 require 方法。不過,受到 CommonJS 模塊化規範的啓發,WEB 端還是逐漸發展起來了 AMD、SystemJS 規範等適合瀏覽器端運行的 JS 模塊化開發規範。

AMD 全稱 Asynchronous module definition,意爲異步的模塊定義,不同於 CommonJS 規範的同步加載,AMD 正如其名所有模塊默認都是異步加載,這也是早期爲了滿足 web 開發的需要,因爲如果在 web 端也使用同步加載,那麼頁面在解析腳本文件的過程中可能使頁面暫停響應。

// index.js
require(['moduleA', 'moduleB'], (moduleA, moduleB) => {
  console.log(moduleB);
});

// moduleA.js
define(require => {
  const m = require('./moduleB');
  setTimeout(() => console.log(m), 1000);
});

// moduleB.js
define(require => {
  const m = new Date().getTime();
  return m;
});

如果想要使用 AMD 規範,我們還需要添加一個符合 AMD 規範的加載器腳本在頁面中,符合 AMD 規範實現的庫很多,比較有名的是 require.js,核心是使用 jsonp 異步加載模塊。

ESModule

前面我們說到的 CommonJS 規範和 AMD 規範有這麼幾個特點:

  1. 語言上層的運行環境中實現的模塊化規範,模塊化規範由環境自己定義。
  2. 互相之間不能共用模塊。例如不能在 Node.js 運行 AMD 模塊,不能直接在瀏覽器運行 CommonJS 模塊。

在 EcmaScript 2015 也就是我們常說的 ES6 之後,JS 有了語言層面的模塊化導入導出關鍵詞與語法以及與之匹配的 ESModule 規範。使用 ESModule 規範,我們可以通過 importexport 兩個關鍵詞來對模塊進行導入與導出。

還是之前的例子,使用 ESModule 規範和新的關鍵詞就需要這樣定義:

// index.js
import './moduleA';
import m from './moduleB';
console.log(m);

// moduleA.js
import m from './moduleB';
setTimeout(() => console.log(m), 1000);

// moduleB.js
export default new Date().getTime();

每個 JS 的運行環境都有一個解析器,否則這個環境也不會認識 JS 語法。它的作用就是用 ECMAScript 的規範去解釋 JS 語法,也就是處理和執行語言本身的內容,例如按照邏輯正確執行 var a = '123';, function func() {console.log('abc');} 之類的內容。

在解析器的上層,每個運行環境都會在解釋器的基礎上封裝一些環境相關的 API。例如 Nodejs 中的 global 對象、process 對象,瀏覽器中的 windowdocument 對象等等。這些運行環境的 API 收到各自規範的影響,例如瀏覽器端的 W3C 規範,它們規定了 window 對象和 document 對象上的 API 內容,以使我們能讓 document.getElementById 這樣的 API 在所有的瀏覽器上運行正常。

ESModule 就屬於 JS Core 層面的規範,而 AMD、CommonJS 是運行環境的規範。所以,要想使運行環境支持 ESModule 其實是比較簡單的,只需要升級自己環境中的 JS Core 解釋引擎到足夠的版本,引擎層面就能認識這種語法,從而不認爲這是個 語法錯誤(syntax error),運行環境中只需要做一些兼容工作即可。

Node.js 在 V12 版本之後纔可以使用 ESModule 規範的模塊,在 V12 沒進入 LTS 之前,我們需要加上 --experimental-modules 的 flag 才能使用這樣的特性,也就是通過 node --experimental-modules index.js 來執行。瀏覽器端 Chrome 61 之後的版本可以開啓支持 ESModule 的選項,只需要通過 `` 這樣的標籤加載即可。

這也就是說,如果想在 Node.js 環境中使用 ESModule,就需要升級 Node.js 到高版本,這相對來說比較容易,畢竟服務器端 Node.js 版本控制在開發人員自己手中。但瀏覽器端具有分佈式的特點,是否能使用這種高版本特性取決於用戶訪問時的版本,而且這種解析器語法層面的內容無法像 AMD 那樣運行時進行兼容,所以想要直接使用就會比較麻煩。

後模塊化的編譯時代

通過前面的分析我們可以看出來,使用 ESModule 的模塊明顯更符合 JS 開發的歷史進程,因爲任何一個支持 JS 的環境,隨着對應解釋器的升級,最終一定會支持 ESModule 的標準。但是, WEB 端受制於用戶使用的瀏覽器版本,我們並不能隨心所欲的隨時使用 JS 的最新特性。爲了能讓我們的新代碼也運行在用戶的老瀏覽器中,社區湧現除了越來越多的工具,它們能靜態將高版本規範的代碼編譯爲低版本規範的代碼,最爲大家所熟知的就是 babel。

它把 JS Core 中高版本規範的語法,也能按照相同語義在靜態階段轉化爲低版本規範的語法,這樣即使是早期的瀏覽器,它們內置的 JS 解釋器也能看懂。

然而,不幸的是,對於模塊化相關的 importexport 關鍵字,babel 最終將會將它編譯爲包含 requireexports 的 CommonJS 規範。 babeljs.cn

這就造成了另一個問題,這樣帶有模塊化關鍵詞的模塊,編譯之後還是沒辦法直接運行在瀏覽器中,因爲瀏覽器端並不能運行 CommonJS 的模塊。爲了能在 WEB 端直接使用 CommonJS 規範的模塊,除了編譯之外,我們還需要一個步驟叫做打包(bundle)

打包工具的作用,就是講模塊化內部實現的細節抹平,無論是 AMD 還是 CommonJS 模塊化規範的模塊,經過打包處理之後能變成能直接運行在 WEB 或 Node.js 的內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章