前端模組化的前世今生

scq000發表於2019-04-03

凡是開發大型應用程式,模組塊必然是不可或缺的一部分。那麼什麼是模組化呢?其實模組化就是將一個複雜的系統分解成多個獨立的模組的程式碼組織方式。在很長的一段時間裡,前端只能通過一系列的<script>標籤來維護我們的程式碼關係,但是一旦我們的專案複雜度提高的時候,這種簡陋的程式碼組織方式便是如噩夢般使得我們的程式碼變得混亂不堪。所以,在開發大型Javascript應用程式的時候,就必須引入模組化機制。由於早期官方並沒有提供統一的模組化解決方案,所以在群雄爭霸的年代,各種前端模組化方案層出不窮。本文將從最早期的IFEE閉包方案到現在的ES6 Modules, 追根溯源,帶你詳細瞭解前端模組化的前世今生。

IIFE

模組化的一大作用就是用來隔離作用域,避免變數衝突。而Javascript沒有語言層面的名稱空間概念,只能將程式碼暴露到全域性作用域下。在刀耕火種的年代,作為指令碼語言的Javascript為了避免全域性變數汙染,只能使用閉包來實現模組化。好在我們可以利用自執行函式(IIFE)來執行程式碼,從而避免變數名洩漏到全域性作用域中:

(function(window) {
	window.jQuery = {
       // 這裡是程式碼 
    };
})(window);
複製程式碼

雖然IIFE可以有效解決命名衝突的問題,但是對於依賴管理,還是束手無策。由於瀏覽器是從上至下執行指令碼,因此為了維持指令碼間的依賴關係,就必須手動維護好script標籤的相對順序。

AMD

AMD (Asynchronous Module Definition)也是一種 JavaScript模組化規範。從名字上可以看出,它主要提供了非同步載入的功能。對於多個JS模組之間的依賴問題,如果使用原生的方式載入程式碼,隨著載入檔案的增多,瀏覽器會長時間地失去響應,而AMD能夠保證被依賴的模組儘早地載入到瀏覽器中,從而提高頁面響應速度。由於該規範原生Javascript無法支援,所以必須使用相應的庫來實現對應的模組化。RequireJS就是實現了該規範的類庫,實際上AMD也是其在推廣過程中的產物。

利用RequireJS來編寫模組,所有的依賴項必須提前宣告好。在匯入模組的時候,也會先載入對應的依賴模組,然後再執行接下來的程式碼,同時AMD模組可以並行載入所有依賴模組,從而很好地提高頁面載入效能:

define('./index.js',function(code){
    // code 就是index.js 返回的內容
    return {
        sayHello: function(name) {
            return "Hello, " + name;
        }
    }
});

複製程式碼

CMD

CMD(Common Module Definition)最初是由阿里的玉伯提出的,同AMD類似,使用CMD模組也需要使用對應的庫SeaJS。SeaJS所要解決的問題和requireJS一樣,但是在使用方式和載入時機上有所不同:

define(function(require) {

   //通過require引用模組

   var path=require.resolve('./cmdDefine');

   alert(path);

});
複製程式碼

CMD載入完某個依賴模組後並不執行,只是下載而已,在所有依賴模組載入完成後進入主邏輯,遇到require語句的時候才執行對應的模組,這樣模組的執行順序和書寫順序是完全一致的。如果使用require.async()方法,可以實現模組的懶載入。

CommonJS

隨著Javasript應用進軍伺服器端,業界急需一種標準的模組化解決方案,於是,CommonJS(www.commonjs.org)應運而生。它最初是由Kevin Dangoor在他的這篇博文中首次提出。

這是一種被廣泛使用的Javascript模組化規範,大家最熟悉的Node.js應用中就是採用這個規範。在Node.js中,內建了module物件用來定義模組, require函式用來載入模組檔案,程式碼如下:

// utils.js 模組定義
var add = function(a, b) {
    return a + b;
};
module.exports = {
    add: add
};

// 載入模組
var utils = require('./utils');
console.log(utils.add(1, 2));
複製程式碼

此種模組化方案特點就是:同步阻塞式載入,無法實現按需非同步載入。另外,如果想要在瀏覽器中使用CommonJS模組就需要使用Browserify進行解析:

npm install browserify -g
browserify utils.js >&emsp;bundle.js
複製程式碼

當然,你也可以使用gulp, webpack等工具進行解析打包後引入到瀏覽器頁面中去。

UMD

上面介紹的CommonJS和AMD等模組化方案都是針對特定的平臺,如果想要實現跨平臺的模組化,就得引入UMD的模組化方式。UMD是通用模組定義(Universal Module Definition)的縮寫,使用該中模組化方案,可以很好地相容AMD, CommonJS等模組化語法。

接下來,讓我們通過一個簡單地例子看一下如何使用和定義UMD模組:

(function(root, factory) {

  if(typeof define === 'function' && define.amd) {

    define(['jquery'], factory);

  } else if(typeof module === 'object' &&

    typeof module.exports === 'object') {

    var jquery = require('jquery');

    module.exports = factory(jquery);

  } else {
      
    root.UmdModule = factory(root.jQuery);
  
  }

}(this, function(jquery) {
	// 現在你可以利用jquery做你想做的事了
    
}));

複製程式碼

這種模組定義方法,可以看做是IIFE的變體。不同的是它倒置了程式碼的執行順序,需要你將所需執行的函式作為第二個引數傳入。由於這種通用模組的適用性強,很多JS框架和類庫都會打包成這種形式的程式碼。

ES6 Modules

對於ES6來說,不必再使用閉包和封裝函式等方式進行模組化支援了。在ES6中,從語法層面就提供了模組化的功能。然而受限於瀏覽器的實現程度,如果想要在瀏覽器中執行,還是需要通過Babel等轉譯工具進行編譯。ES6提供了importexport命令,分別對應模組的匯入和匯出功能。具體例項如下:

// demo-export.js 模組定義
var name = "scq000"
var sayHello = (name) => {
  console.log("Hi," + name);
}
export {name, sayHello};

// demo-import.js 使用模組
import {sayHello} from "./demo-export";
sayHello("scq000");
複製程式碼

對於具體的語法細節,想必大家在日常使用過程中都已經輕車熟路了。但對於ES6模組化來說,有以下幾點特性是需要記住的:

  • ES6使用的是基於檔案的模組。所以必須一個檔案一個模組,不能將多個模組合併到單個檔案中去。
  • ES6模組API是靜態的,一旦匯入模組後,無法再在程式執行過程中增添方法。
  • ES6模組採用引用繫結(可以理解為指標)。這點和CommonJS中的值繫結不同,如果你的模組在執行過程中修改了匯出的變數值,就會反映到使用模組的程式碼中去。所以,不推薦在模組中修改匯出值,匯出的變數應該是靜態的。
  • ES6模組採用的是單例模式,每次對同一個模組的匯入其實都指向同一個例項。

Webpack中的模組化方案

作為現代化的前端構建工具,Webpack還提供了豐富的功能能夠使我們更加輕易地實現模組化。利用Webpack,你不僅可以將Javascript檔案進行模組化,同時還能針對圖片,css等靜態資源進行模組化。你可以在程式碼裡使用CommonJS, ES6等模組化語法,打包的時候你也可以根據需求選擇打包型別,如UMD, AMD等:

module.exports = {
  //...
  output: {
    library: 'librayName',
    libraryTarget: 'umd', // 配置輸出格式
    filename: 'bundle.js'
  }
};
複製程式碼

另外,ES6模組好處很多,但是並不支援按需載入的功能, 而按需載入又是Web效能優化中重要的一個環節。好在我們可以藉助Webpack來彌補這一缺陷。Webpack v1版本提供了require.ensureAPI, 而2.x之後使用了import()函式來實現非同步載入。具體的程式碼示例可以檢視我之前所寫的前端效能優化之載入技術 這篇文章。

總結

模組化方案解決了程式碼之間錯綜複雜的依賴關係,不僅降低了開發難度同時也讓開發者將精力更多地集中在業務開發中。隨著ES6標準的推出,模組化直接成為了Javascript語言規範中的一部分。這也意味著CommonJS, AMD, CMD等模組化方案終將退出歷史的舞臺。當然,要實現完全ES6模組化的使用,還需要一段長時間的等待。那麼,在這段過渡的時間裡,我們可能仍然需要維護舊有的程式碼,使用傳統的模組化方案來構建應用。對於前端工程師來說,系統地瞭解主流的模組化方案就顯得十分必要了。最後,讓我們再一次回顧一下各種模組化方式的特點:

模組化方案 載入 同步/非同步 瀏覽器 服務端 模組定義 模組引入
IFEE 取決於程式碼 取決於程式碼 支援 支援 IFEE 名稱空間
AMD 提前預載入 非同步 支援 構建工具r.js define require
CMD 按需載入 延遲執行 支援 構建工具spm define define
Common 值拷貝,執行時載入 同步 原生不支援,需要使用browserify提前打包編譯 原生支援 module.exports require
UMD 取決於程式碼 取決於程式碼 支援 支援 IFEE 名稱空間
ES Modules(ES6) 實時繫結,動態繫結,編譯時輸出 同步 需用babel轉譯 需用babel轉譯 export import

參考資料

javascript.ruanyifeng.com/nodejs/modu…

《你不知道的Javascript》

www.infoq.cn/article/es6…

——本文首發於個人公眾號,轉載請註明出處———

微信掃描二維碼,關注我的公眾號
最後,歡迎大家關注我的公眾號,一起學習交流。

相關文章