前端模組化:CommonJS,AMD,CMD,ES6(轉載)

Leon Aries發表於2019-03-19

模組化的開發方式可以提高程式碼複用率,方便進行程式碼的管理。通常一個檔案就是一個模組,有自己的作用域,只向外暴露特定的變數和函式。目前流行的js模組化規範有CommonJS、AMD、CMD以及ES6的模組系統。參見阮一峰老師的文章 module-loader

一、CommonJS

Node.js是commonJS規範的主要實踐者,它有四個重要的環境變數為模組化的實現提供支援:moduleexportsrequireglobal實際使用時,用module.exports定義當前模組對外輸出的介面(不推薦直接用exports),用require載入模組。

// 定義模組math.js
var basicNum = 0;
function add(a, b) {
  return a + b;
}
module.exports = { //在這裡寫上需要向外暴露的函式、變數
  add: add,
  basicNum: basicNum
}

// 引用自定義的模組時,引數包含路徑,可省略.js
var math = require('./math');
math.add(2, 5);

// 引用核心模組時,不需要帶路徑
var http = require('http');
http.createService(...).listen(3000);
複製程式碼複製程式碼

commonJS用同步的方式載入模組。在服務端,模組檔案都存在本地磁碟,讀取非常快,所以這樣做不會有問題。但是在瀏覽器端,限於網路原因,更合理的方案是使用非同步載入。

二、AMD和require.js

AMD規範採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。這裡介紹用require.js實現AMD規範的模組化:用require.config()指定引用路徑等,用define()定義模組,用require()載入模組。

首先我們需要引入require.js檔案和一個入口檔案main.js。main.js中配置require.config()並規定專案中用到的基礎模組。

/** 網頁中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>

/** main.js 入口檔案/主模組 **/
// 首先用config()指定各模組路徑和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //實際路徑為js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 執行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});
複製程式碼複製程式碼

引用模組的時候,我們將模組名放在[]中作為reqiure()的第一引數;如果我們定義的模組本身也依賴其他模組,那就需要將它們放在[]中作為define()的第一引數。

// 定義math.js模組
define(function () {
    var basicNum = 0;
    var add = function (x, y) {
        return x + y;
    };
    return {
        add: add,
        basicNum :basicNum
    };
});
// 定義一個依賴underscore.js的模組
define(['underscore'],function(_){
  var classify = function(list){
    _.countBy(list,function(num){
      return num > 30 ? 'old' : 'young';
    })
  };
  return {
    classify :classify
  };
})

// 引用模組,將模組放在[]內
require(['jquery', 'math'],function($, math){
  var sum = math.add(10,20);
  $("#sum").html(sum);
});
複製程式碼複製程式碼

三、CMD和sea.js

require.js在申明依賴的模組時會在第一之間載入並執行模組內的程式碼:

define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
    // 等於在最前面宣告並初始化了要用到的所有模組
    if (false) {
      // 即便沒用到某個模組 b,但 b 還是提前執行了
      b.foo()
    } 
});
複製程式碼複製程式碼

CMD是另一種js模組化方案,它與AMD很類似,不同點在於:AMD 推崇依賴前置、提前執行,CMD推崇依賴就近、延遲執行。此規範其實是在sea.js推廣過程中產生的。

/** AMD寫法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等於在最前面宣告並初始化了要用到的所有模組
    a.doSomething();
    if (false) {
        // 即便沒用到某個模組 b,但 b 還是提前執行了
        b.doSomething()
    } 
});

/** CMD寫法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要時申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

/** sea.js **/
// 定義模組 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 載入模組
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});
複製程式碼複製程式碼

四、ES6 Module

ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,旨在成為瀏覽器和伺服器通用的模組解決方案。其模組功能主要由兩個命令構成:exportimportexport命令用於規定模組的對外介面,import命令用於輸入其他模組提供的功能。

/** 定義模組 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };

/** 引用模組 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}
複製程式碼複製程式碼

如上例所示,使用import命令的時候,使用者需要知道所要載入的變數名或函式名。其實ES6還提供了export default命令,為模組指定預設輸出,對應的import語句不需要使用大括號。這也更趨近於ADM的引用寫法。

/** export default **/
//定義輸出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}
複製程式碼複製程式碼

ES6的模組不是物件,import命令會被 JavaScript 引擎靜態分析,在編譯時就引入模組程式碼,而不是在程式碼執行時載入,所以無法實現條件載入。也正因為這個,使得靜態分析成為可能。

五、 ES6 模組與 CommonJS 模組的差異

1. CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。

  • CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。
  • ES6 模組的執行機制與 CommonJS 不一樣。JS 引擎對指令碼靜態分析的時候,遇到模組載入命令import,就會生成一個只讀引用。等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值。換句話說,ES6 的import有點像 Unix 系統的“符號連線”,原始值變了,import載入的值也會跟著變。因此,ES6 模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。

2. CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。

  • 執行時載入: CommonJS 模組就是物件;即在輸入時是先載入整個模組,生成一個物件,然後再從這個物件上面讀取方法,這種載入稱為“執行時載入”。

  • 編譯時載入: ES6 模組不是物件,而是通過 export 命令顯式指定輸出的程式碼,import時採用靜態命令的形式。即在import時可以指定載入某個輸出值,而不是載入整個模組,這種載入稱為“編譯時載入”。

CommonJS 載入的是一個物件(即module.exports屬性),該物件只有在指令碼執行完才會生成。而 ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。


相關文章