JavaScript 模組化總結

橡樹上發表於2019-04-19

關鍵詞: AMD、CMD、UMD、CommonJS、ES Module

規範JavaScript的模組定義和載入機制,降低學習和使用各種框架的門檻,能夠以一種統一的方式去定義和使用模組,提高開發效率,降低了應用維護成本。

目錄:

模組化的歷史

想當初,Brendan Eich 只用了十天就創造了 JavaScript 這門語言,誰曾想這門一直被看作玩具性質的語言在近幾年獲得了爆發性地發展,從瀏覽器端擴充套件到伺服器,再到 native 端,變得越來越火熱。而這門語言創造當初的諸多限制也在前端工程化的今天被放大,社群也在積極推動其變革。實現模組化的開發正是其中最大的需求,本文梳理 JavaScript 模組化開發的歷史和未來,以作學習之用。

JavaScript 模組化的發展歷程,是以 2009 年 CommonJS 的出現為分水嶺,這一規範極大地推動前端發展。在1999年至2009年期間,模組化探索都是基於語言層面的優化,2009 年後前端開始大量使用預編譯。

刀耕火種的原始時代(1999 - 2009)

在 1999 年的時候,那會還沒有全職的前端工程師,寫 JS 是直接將變數定義在全域性,做的好一些的或許會做一些檔案目錄規劃,將資源歸類整理,這種方式被稱為直接定義依賴,舉個例子:

// greeting.js
var helloInLang = {
  en: 'Hello world!',
  es: '¡Hola mundo!',
  ru: 'Привет мир!'
};
function writeHello(lang) {
  document.write(helloInLang[lang]);
}

// third_party_script.js
function writeHello() {
  document.write('The script is broken');
}
複製程式碼
// index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Basic example</title>
  <script src="./greeting.js"></script>
  <script src="./third_party_script.js"></script>
</head>
複製程式碼

但是,即使有規範的目錄結構,也不能避免由此而產生的大量全域性變數,這就導致了一不小心就會有變數衝突的問題,就好比上面這個例子中的 writeHello

於是在 2002 年左右,有人提出了名稱空間模式的思路,用於解決遍地的全域性變數,將需要定義的部分歸屬到一個物件的屬性上,簡單修改上面的例子,就能實現這種模式:

// greeting.js
var app = {};
app.helloInLang = {
  en: 'Hello world!',
  es: '¡Hola mundo!',
  ru: 'Привет мир!'
};
app.writeHello = function (lang) {
  document.write(helloInLang[lang]);
}

// third_party_script.js
function writeHello() {
  document.write('The script is broken');
}
複製程式碼

不過這種方式,毫無隱私可言,本質上就是全域性物件,誰都可以來訪問並且操作,一點都不安全。

2003 年左右就有人提出利用 IIFE 結合 Closures 特性,以此解決私有變數的問題,這種模式被稱為閉包模組化模式:

// greeting.js
var greeting = (function() {
  var module = {};
  var helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!',
  };

  module.getHello = function(lang) {
    return helloInLang[lang];
  };

  module.writeHello = function(lang) {
    document.write(module.getHello(lang));
  };

  return module;
})();
複製程式碼

IIFE 可以形成一個獨立的作用域,其中宣告的變數,僅在該作用域下,從而達到實現私有變數的目的,就如上面例子中的 helloInLang,在該 IIFE 外是不能直接訪問和操作的,可以通過暴露一些方法來訪問和操作,比如說上面例子裡面的 getHellowriteHello2 個方法,這就是所謂的 Closures。

同時,不同模組之間的引用也可以通過引數的形式來傳遞:

// x.js
// @require greeting.js
var x = (function(greeting) {
  var module = {};

  module.writeHello = function(lang) {
    document.write(greeting.getHello(lang));
  };

  return module;
})(greeting);
複製程式碼

此外使用 IIFE,還有2個好處:

  1. 提高效能:通過 IIFE 的引數傳遞常用全域性物件 window、document,在作用域內引用這些全域性物件。JavaScript 直譯器首先在作用域內查詢屬性,然後一直沿著鏈向上查詢,直到全域性範圍,因此將全域性物件放在 IIFE 作用域內可以提升js直譯器的查詢速度和效能;
  2. 壓縮空間:通過引數傳遞全域性物件,壓縮時可以將這些全域性物件匿名為一個更加精簡的變數名;

除了這些方式,還有其他的如模版依賴定義註釋依賴定義外部依賴定義,不是很常見,但其本質都是想在語言層面解決模組化的問題。

不過,這些方案,雖然解決了依賴關係的問題,但是沒有解決如何管理這些模組,或者說在使用時清晰描述出依賴關係,這點還是沒有被解決,可以說是少了一個管理者。

沒有管理者的時候,在實際專案中,得手動管理第三方的庫和專案封裝的模組,就像下面這樣把所有需要的 JS 檔案一個個按照依賴的順序載入進來:

<script src="zepto.js"></script>
<script src="jhash.js"></script>
<script src="fastClick.js"></script>
<script src="iScroll.js"></script>
<script src="underscore.js"></script>
<script src="handlebar.js"></script>
<script src="datacenter.js"></script>
<script src="deferred.js"></script>
<script src="util/wxbridge.js"></script>
<script src="util/login.js"></script>
<script src="util/base.js"></script>
<script src="util/city.js"></script>
複製程式碼

對於這個問題,社群出現了新的工具,如 LABjs、YUI。YUI 作為昔日前端領域的佼佼者,很好的糅合了名稱空間模式沙箱模式,如以下的例子:

// YUI - 編寫模組
YUI.add('dom', function(Y) {
  Y.DOM = { ... }
})

// YUI - 使用模組
YUI().use('dom', function(Y) {
  Y.DOM.doSomeThing();
  // use some methods DOM attach to Y
})

// hello.js
YUI.add('hello', function(Y){
    Y.sayHello = function(msg){
        Y.DOM.set(el, 'innerHTML', 'Hello!');
    }
},'3.0.0',{
    requires:['dom']
})

// main.js
YUI().use('hello', function(Y){
    Y.sayHello("hey yui loader");
})
複製程式碼

YUI 團隊還提供的一系列用於 JS 壓縮、混淆、請求合併(合併資源需要 server 端配合)等效能優化的工具,說其是現有 JS 模組化的鼻祖一點都不過分。

不過隨著 Node.js 的到來,新出的 CommonJS 規範的落地,以及各種前端工具、解決方案的出現,才真正使得前端開發大放光芒。

大步踏進工業化 (2009 - 至今)

CommonJS 的出現真正使得前端進入工業化時代。前面說了,2009 年以前的各種模組化方案雖然始終停留在語言層面上,雖然也有 YUI 這樣的工具,但還不足以成為引領潮流的工具。究其原因,還是因為前端工程複雜度還沒積累到一定程度,隨著 Node.js 的出現,JS 涉足的領域轉向後端,加上 Web app 變得越來越複雜,工程發展到一定階段,要出現的必然會出現。

CommonJS 是一套同步的方案,它考慮的是在服務端執行的Node.js,主要是通過 require 來載入依賴項,通過 exports 或者 module.exports 來暴露介面或者資料的方式。

由於在服務端可以直接讀取磁碟上的檔案,所以能做到同步載入資源,但在瀏覽器上是通過 HTTP 方式獲取資源,複雜的網路情況下無法做到同步,這就導致必須使用非同步載入機制。這裡發展出兩個有影響力的方案:

  • 基於 AMD 的 RequireJS
  • 基於 CMD 的 SeaJS

它們分別在瀏覽器實現了definerequiremodule的核心功能,雖然兩者的目標是一致的,但是實現的方式或者說是思路,還是有些區別的,AMD 偏向於依賴前置,CMD 偏向於用到時才執行的思路,從而導致了依賴項的載入和執行時間點會不同。

// CMD
define(function (require) {
    var a = require('./a'); // <- 執行到此處才開始載入並執行模組a
    var b = require('./b'); // <- 執行到此處才開始載入並執行模組b
    // more code ..
})
複製程式碼
// AMD
define(
    ['./a', './b'], // <- 前置宣告,也就是在主體執行前就已經載入並執行了模組a和模組b
    function (a, b) {
        // more code ..
    }
)
複製程式碼

這裡也有不少爭議的地方,在於 CommonJS 社群認為 AMD 模式破壞了規範,反觀 CMD 模式,簡單的去除 define 的外包裝,這就是標準的 CommonJS 實現,所以說 CMD 是最貼近 CommonJS 的非同步模組化方案。不過 AMD 的社群資源比 CMD 更豐富,這也是 AMD 更加流行的一個原因。

此外同一時期還出現了一個 UMD 的方案,其實它就是 AMD 與 CommonJS 的集合體,通過 IIFE 的前置條件判斷,使一個模組既可以在瀏覽器執行,也可以在 Node.js 中執行,舉個例子:

// UMD
(function(define) {
    define(function () {
        var helloInLang = {
            en: 'Hello world!',
            es: '¡Hola mundo!',
            ru: 'Привет мир!'
        };

        return {
            sayHello: function (lang) {
                return helloInLang[lang];
            }
        };
    });
}(
    typeof module === 'object' && module.exports && typeof define !== 'function' ?
    function (factory) { module.exports = factory(); } :
    define
));
複製程式碼

不過這個用的比較少,僅作了解。

2015年6月,ECMAScript2015 釋出了,JavaScript 終於在語言標準的層面上,實現了模組功能,使得在編譯時就能確定模組的依賴關係,以及其輸入和輸出的變數,不像 CommonJS、AMD 之類的需要在執行時才能確定,成為瀏覽器和伺服器通用的模組解決方案。

// lib/greeting.js
const helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

export const getHello = (lang) => (
    helloInLang[lang];
);

export const sayHello = (lang) => {
    console.log(getHello(lang));
};

// hello.js
import { sayHello } from './lib/greeting';

sayHello('ru');
複製程式碼

與 CommonJS 用 require() 方法載入模組不同,在 ES Module 中,import 命令可以具體指定載入模組中用 export 命令暴露的介面(不指定具體的介面,預設載入 export default),沒有指定的是不會載入的,因此會在編譯時就完成模組的載入,這種載入方式稱為編譯時載入或者靜態載入

而 CommonJS 的 require() 方法是在執行時才載入的:


// lib/greeting.js
const helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};
const getHello = function (lang) {
    return helloInLang[lang];
};

exports.getHello = getHello;
exports.sayHello = function (lang) {
    console.log(getHello(lang))
};

// hello.js
const sayHello = require('./lib/greeting').sayHello;

sayHello('ru');
複製程式碼

可以看出,CommonJS 中是將整個模組作為一個物件引入,然後再獲取這個物件上的某個屬性。

因此 ES Module 的編譯時載入,在效率上面會提高不少,此外,還會帶來一些其它的好處,比如引入巨集(macro)和型別檢驗(type system)這些只能靠靜態分析實現的功能。

不過由於 ES Module 在低版本的 Node.js 和瀏覽器上支援度有待加強,所以一般還是通過 Babel 進行轉換成 es5 的語法,相容更多的平臺。

各種模組化方案出現的時間線

  • 1999: 直接定義依賴
  • 2002: 名稱空間模式
  • 2003: 閉包模組化模式
  • 2006: 模版依賴定義
  • 2006:註釋依賴定義
  • 2007:外部依賴定義
  • 2009:Sandbox 模式
  • 2009:依賴注入
  • 2009: ?CommonJS 規範
  • 2009: ?AMD 規範,
  • 2009: ?CMD 規範,差不多跟 AMD 規範同樣時間出現,都是為了解決瀏覽器端模組化問題,它是由 sea.js 在推廣過程中對模組定義的規範化產出。
  • 2011: UMD 規範
  • 2012: Labeled Modules
  • 2013: YModules
  • 2015: ?ES Module

CommonJS

介紹

Node 應用由模組組成,採用 CommonJS 模組規範。

每個檔案就是一個模組,有自己的作用域。在一個檔案裡面定義的變數、函式、類,都是私有的,對其他檔案不可見。

CommonJS 規範規定,每個模組內部,module 變數代表當前模組。這個變數是一個物件,它的 exports 屬性(即 module.exports )是對外的介面。載入某個模組,其實是載入該模組的 module.exports 屬性。

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
複製程式碼

require方法用於載入模組。

var example = require('./example.js');

console.log(example.x); // 5
console.log(example.addX(1)); // 6
複製程式碼

特點

  • 所有程式碼都執行在模組作用域,不會汙染全域性作用域。
  • 模組可以多次載入,但是隻會在第一次載入時執行一次,然後執行結果就被快取了,以後再載入,就直接讀取快取結果。要想讓模組再次執行,必須清除快取。
  • 模組載入的順序,按照其在程式碼中出現的順序。

module 物件

Node 內部提供一個 Module 構建函式。所有模組都是 Module 的例項。

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  // ...
}
複製程式碼

每個模組內部,都有一個 module 物件,代表當前模組。它有以下屬性:

  • module.id 模組的識別符,通常是帶有絕對路徑的模組檔名。
  • module.filename 模組的檔名,帶有絕對路徑。
  • module.loaded 返回一個布林值,表示模組是否已經完成載入。
  • module.parent 返回一個物件,表示呼叫該模組的模組。
  • module.children 返回一個陣列,表示該模組要用到的其他模組。
  • module.exports 表示模組對外輸出的值

module.exports 屬性表示當前模組對外輸出的介面,其他檔案載入該模組,實際上就是讀取 module.exports 變數。

為了方便,Node 為每個模組提供一個 exports 變數,指向 module.exports。這等同在每個模組頭部,有一行這樣的命令:

var exports = module.exports;
複製程式碼

造成的結果是,在對外輸出模組介面時,可以向 exports 物件新增方法。

exports.area = function (r) {
  return Math.PI * r * r;
};

exports.circumference = function (r) {
  return 2 * Math.PI * r;
};
複製程式碼

注意,不能直接將 exports 變數指向一個值,因為這樣等於切斷了 exportsmodule.exports 的聯絡。

// 無效程式碼
exports.hello = function() {
  return 'hello';
};

module.exports = 'Hello world';
複製程式碼

上面程式碼中,hello 函式是無法對外輸出的,因為 module.exports 被重新賦值了。

這意味著,如果一個模組的對外介面,就是一個單一的值,不能使用 exports 輸出,只能使用 module.exports 輸出。

module.exports = function (x){ console.log(x);};
複製程式碼

目錄的載入規則

通常,我們會把相關的檔案會放在一個目錄裡面,便於組織。這時,最好為該目錄設定一個入口檔案,讓 require 方法可以通過這個入口檔案,載入整個目錄。

在目錄中放置一個 package.json 檔案,並且將入口檔案寫入 main 欄位。下面是一個例子。

// package.json
{ 
  "name" : "some-library",
  "main" : "./lib/some-library.js" 
}
複製程式碼

require 發現引數字串指向一個目錄以後,會自動檢視該目錄的 package.json 檔案,然後載入 main 欄位指定的入口檔案。如果 package.json 檔案沒有 main 欄位,或者根本就沒有 package.json 檔案,則會載入該目錄下的 index.js 檔案或 index.node 檔案。

模組的快取

第一次載入某個模組時,Node會快取該模組。以後再載入該模組,就直接從快取取出該模組的 module.exports 屬性。

require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"
複製程式碼

上面程式碼中,連續三次使用 require 命令,載入同一個模組。第二次載入的時候,為輸出的物件新增了一個 message 屬性。但是第三次載入的時候,這個 message 屬性依然存在,這就證明 require 命令並沒有重新載入模組檔案,而是輸出了快取。

如果想要多次執行某個模組,可以讓該模組輸出一個函式,然後每次 require 這個模組的時候,重新執行一下輸出的函式。

所有快取的模組儲存在 require.cache 之中,如果想刪除模組的快取,可以像下面這樣寫。

// 刪除指定模組的快取
delete require.cache[moduleName];

// 刪除所有模組的快取
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})
複製程式碼

注意,快取是根據絕對路徑識別模組的,如果同樣的模組名,但是儲存在不同的路徑,require 命令還是會重新載入該模組。

模組的載入機制

CommonJS 模組的載入機制是,輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。請看下面這個例子。

下面是一個模組檔案lib.js

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
複製程式碼

上面程式碼輸出內部變數 counter 和改寫這個變數的內部方法 incCounter

然後,載入上面的模組。

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3
複製程式碼

上面程式碼說明,counter 輸出以後,lib.js 模組內部的變化就影響不到 counter 了。

AMD

介紹

AMD 全稱為 Asynchromous Module Definition(非同步模組定義)。 AMD 是 RequireJS 在推廣過程中對模組定義的規範化產出,它是一個在瀏覽器端模組化開發的規範。 AMD 模式可以用於瀏覽器環境並且允許非同步載入模組,同時又能保證正確的順序,也可以按需動態載入模組。

規範介紹

模組通過 define 函式定義在閉包中,格式如下:

define(id?: String, dependencies?: String[], factory: Function|Object);
複製程式碼

id 是模組的名字,它是可選的引數。

dependencies 指定了所要依賴的模組列表,它是一個陣列,也是可選的引數,每個依賴的模組的輸出將作為引數一次傳入 factory 中。如果沒有指定 dependencies,那麼它的預設值是 ["require", "exports", "module"]

define(function(require, exports, module) {})
複製程式碼

factory 是最後一個引數,它包裹了模組的具體實現,它是一個函式或者物件。如果是函式,那麼它的返回值就是模組的輸出介面或值。

用例:

定義一個名為 myModule 的模組,它依賴 jQuery 模組:

// 定義
define('myModule', ['jquery'], function($) {
    // $ 是 jquery 模組的輸出
    $('body').text('hello world');
});
// 使用
require(['myModule'], function(myModule) {});
複製程式碼

定義一個沒有 id 值的匿名模組,通常作為應用的啟動函式:

define(['jquery'], function($) {
    $('body').text('hello world');
});
複製程式碼

依賴多個模組的定義:

define(['jquery', './math.js'], function($, math) {
    // $ 和 math 一次傳入 factory
    $('body').text('hello world');
});
複製程式碼

模組輸出:

define(['jquery'], function($) {

    var HelloWorldize = function(selector){
        $(selector).text('hello world');
    };

    // HelloWorldize 是該模組輸出的對外介面
    return HelloWorldize;
});
複製程式碼

在模組定義內部引用依賴:

define(function(require) {
    var $ = require('jquery');
    $('body').text('hello world');
});
複製程式碼

RequireJS 的介紹

RequireJS 可以看作是對 AMD 規範的具體實現,它的用法和上節所展示的有所區別。

下載地址:requirejs.org/docs/downlo…

下面簡單介紹一下其用法:

  1. 在 index.html 中引用 RequireJS:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>requirejs test</title>
  </head>
  <body>
    <div id="messageBox"></div>
    <button id="btn" type="button" name="button">點選</button>
    <script data-main="js/script/main.js" src="js/lib/require.js"></script>
  </body>
</html>
複製程式碼

這裡的 script 標籤,除了指定 RequireJS 路徑外,還有個 data-main 屬性,這屬性指定在載入完 RequireJS 後,就用 RequireJS 載入該屬性值指定路徑下的 JS 檔案並執行,所以一般該 JS 檔案稱為主 JS 檔案(其 .js 字尾可以省略)。

  1. main.js
// 配置檔案
require.config({
    baseUrl: 'js',
    paths: {
        jquery: 'lib/jquery-1.11.1',
    }
});

// 載入模組
require(['jquery', 'script/hello'],function ($, hello) {
    $("#btn").click(function(){
      hello.showMessage("test");
    });
});
複製程式碼
  1. hello.js
// 定義模組
define(['jquery'],function($){
    //變數定義區
    var moduleName = "hello module";
    var moduleVersion = "1.0";
 
    //函式定義區
    var showMessage = function(name){
        if(undefined === name){
            return;
        }else{
            $('#messageBox').html('歡迎訪問 ' + name);
        }
    };
 
    //暴露(返回)本模組API
    return {
        "moduleName":moduleName,
        "version": moduleVersion,
        "showMessage": showMessage
    }
});
複製程式碼

我們通過 define 方法定義一個 js 模組,並通過 return 對外暴露出介面(兩個屬性,一個方法)。同時該模組也是依賴於 jQuery。

RequireJS 支援使用 require.config 來配置專案,具體 API 使用方法見官網文件或網上資料,這裡只做基本介紹。

CMD

介紹

在前端的模組化發展上,還有另一種與 AMD 相提並論的規範,這就是 CMD:

CMD 即 Common Module Definition 通用模組定義。 CMD 是 SeaJS 在推廣過程中對模組定義的規範化產出。 CMD 規範的前身是 Modules/Wrappings 規範。

規範介紹

在 CMD 規範中,一個模組就是一個檔案。程式碼的書寫格式如下:

define(factory);
複製程式碼

1. define Function

define 是一個全域性函式,用來定義模組。

define(factory)

define 接受 factory 引數,factory 可以是一個函式,也可以是一個物件或字串。

factory 為物件、字串時,表示模組的介面就是該物件、字串。比如可以如下定義一個 JSON 資料模組:

define({ "foo": "bar" });
複製程式碼

也可以通過字串定義模板模組:

define('I am a template. My name is {{name}}.');
複製程式碼

factory 為函式時,表示是模組的構造方法。執行該構造方法,可以得到模組向外提供的介面。factory 方法在執行時,預設會傳入三個引數:requireexportsmodule

define(function(require, exports, module) {
  // 模組程式碼
});
複製程式碼

define(id?, deps?, factory)

define 也可以接受兩個以上引數。字串 id 表示模組標識,陣列 deps 是模組依賴。比如:

define('hello', ['jquery'], function(require, exports, module) {
  // 模組程式碼
});
複製程式碼

iddeps 引數可以省略。省略時,可以通過構建工具自動生成。

注意:帶 id 和 deps 引數的 define 用法不屬於 CMD 規範,而屬於 Modules/Transport 規範。

define.cmd

一個空物件,可用來判定當前頁面是否有 CMD 模組載入器:

if (typeof define === "function" && define.cmd) {
  // 有 Sea.js 等 CMD 模組載入器存在
}
複製程式碼

2. require Function

requirefactory 函式的第一個引數。

require(id)

require 是一個方法,接受模組標識作為唯一引數,用來獲取其他模組提供的介面。

define(function(require, exports) {

  // 獲取模組 a 的介面
  var a = require('./a');

  // 呼叫模組 a 的方法
  a.doSomething();

});
複製程式碼

require.async(id, callback?)

require.async 方法用來在模組內部非同步載入模組,並在載入完成後執行指定回撥。callback 引數可選。

define(function(require, exports, module) {

  // 非同步載入一個模組,在載入完成時,執行回撥
  require.async('./b', function(b) {
    b.doSomething();
  });

  // 非同步載入多個模組,在載入完成時,執行回撥
  require.async(['./c', './d'], function(c, d) {
    c.doSomething();
    d.doSomething();
  });

});
複製程式碼

注意require 是同步往下執行,require.async 則是非同步回撥執行。require.async 一般用來載入可延遲非同步載入的模組。

require.resolve(id)

使用模組系統內部的路徑解析機制來解析並返回模組路徑。該函式不會載入模組,只返回解析後的絕對路徑。

define(function(require, exports) {

  console.log(require.resolve('./b'));
  // ==> http://example.com/path/to/b.js

});
複製程式碼

這可以用來獲取模組路徑,一般用在外掛環境或需動態拼接模組路徑的場景下。

3. exports Object

exports 是一個物件,用來向外提供模組介面。

define(function(require, exports) {

  // 對外提供 foo 屬性
  exports.foo = 'bar';

  // 對外提供 doSomething 方法
  exports.doSomething = function() {};

});
複製程式碼

除了給 exports 物件增加成員,還可以使用 return 直接向外提供介面。

define(function(require) {

  // 通過 return 直接提供介面
  return {
    foo: 'bar',
    doSomething: function() {}
  };

});
複製程式碼

如果 return 語句是模組中的唯一程式碼,還可簡化為:

define({
  foo: 'bar',
  doSomething: function() {}
});
複製程式碼

特別注意:下面這種寫法是錯誤的!

define(function(require, exports) {

  // 錯誤用法!!!
  exports = {
    foo: 'bar',
    doSomething: function() {}
  };

});
複製程式碼

正確的寫法是用 return 或者給 module.exports 賦值:

define(function(require, exports, module) {

  // 正確寫法
  module.exports = {
    foo: 'bar',
    doSomething: function() {}
  };

});
複製程式碼

提示:exports 僅僅是 module.exports 的一個引用。在 factory 內部給 exports 重新賦值時,並不會改變 module.exports 的值。因此給 exports 賦值是無效的,不能用來更改模組介面。

4. module Object

module 是一個物件,上面儲存了與當前模組相關聯的一些屬性和方法。

module.id String

模組的唯一標識。

define('id', [], function(require, exports, module) {

  // 模組程式碼

});
複製程式碼

上面程式碼中,define 的第一個引數就是模組標識。

module.uri String

根據模組系統的路徑解析規則得到的模組絕對路徑。

define(function(require, exports, module) {

  console.log(module.uri); 
  // ==> http://example.com/path/to/this/file.js

});
複製程式碼

一般情況下(沒有在 define 中手寫 id 引數時),module.id 的值就是 module.uri,兩者完全相同。

module.dependencies Array

dependencies 是一個陣列,表示當前模組的依賴。

module.exports Object

當前模組對外提供的介面。

傳給 factory 構造方法的 exports 引數是 module.exports 物件的一個引用。只通過 exports 引數來提供介面,有時無法滿足開發者的所有需求。 比如當模組的介面是某個類的例項時,需要通過 module.exports 來實現:

define(function(require, exports, module) {

  // exports 是 module.exports 的一個引用
  console.log(module.exports === exports); // true

  // 重新給 module.exports 賦值
  module.exports = new SomeClass();

  // exports 不再等於 module.exports
  console.log(module.exports === exports); // false

});
複製程式碼

注意:對 module.exports 的賦值需要同步執行,不能放在回撥函式裡。下面這樣是不行的:

// x.js
define(function(require, exports, module) {

  // 錯誤用法
  setTimeout(function() {
    module.exports = { a: "hello" };
  }, 0);

});
複製程式碼

SeaJS 的介紹

文件地址:Sea.js - A Module Loader for the Web

簡單入手:

  1. index.html
<!DOCTYPE html>
<html>
    <head>
        <script type="text/javascript" src="sea.js"></script>
        <script type="text/javascript">
          // seajs 的簡單配置
          seajs.config({
            base: "../sea-modules/",
            alias: {
              "jquery": "jquery/jquery/1.10.1/jquery.js"
            }
          })

          // 載入入口模組
          seajs.use("../static/hello/src/main")
        </script>
    </head>
    <body>
    </body>
</html>
複製程式碼
  1. main.js
// 所有模組都通過 define 來定義
define(function(require, exports, module) {

  // 通過 require 引入依賴
  var $ = require('jquery');
  var Spinning = require('./spinning');

  // 通過 exports 對外提供介面
  exports.doSomething = ...

  // 或者通過 module.exports 提供整個介面
  module.exports = ...

});
複製程式碼

UMD

特點:相容 AMD 和 CommonJS 規範的同時,還相容全域性引用的方式

常規寫法:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        //AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        //Node, CommonJS之類的
        module.exports = factory(require('jquery'));
    } else {
        //瀏覽器全域性變數(root 即 window)
        root.returnExports = factory(root.jQuery);
    }
}(this, function ($) {
    //方法
    function myFunc(){};
    //暴露公共方法
    return myFunc;
}));
複製程式碼

ES Module

介紹

在 ES Module 之前,社群制定了一些模組載入方案,最主要的有 CommonJS 和 AMD 兩種。前者用於伺服器,後者用於瀏覽器。ES Module 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規範,成為瀏覽器和伺服器通用的模組解決方案。

ES Module 的設計思想是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。CommonJS 和 AMD 模組,都只能在執行時確定這些東西。

CommonJS 和 AMD 模組,其本質是在執行時生成一個物件進行匯出,稱為“執行時載入”,沒法進行“編譯優化”,而 ES Module 不是物件,而是通過 export 命令顯式指定輸出的程式碼,再通過 import 命令輸入。這稱為“編譯時載入”或者靜態載入,即 ES Module 可以在編譯時就完成模組載入,效率要比 CommonJS 模組的載入方式高。當然,這也導致了沒法引用 ES Module 模組本身,因為它不是物件。

由於 ES Module 是編譯時載入,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入巨集(macro)和型別檢驗(type system)這些只能靠靜態分析實現的功能。

除了靜態載入帶來的各種好處,ES Module 還有以下好處:

  • 不再需要 UMD 模組格式了,將來伺服器和瀏覽器都會支援 ES Module 格式。目前,通過各種工具庫,其實已經做到了這一點。
  • 將來瀏覽器的新 API 就能用模組格式提供,不再必須做成全域性變數或者 navigator 物件的屬性。
  • 不再需要物件作為名稱空間(比如 Math 物件),未來這些功能可以通過模組提供。

特點

  • 靜態編譯
  • 輸出的值引用,而非值拷貝
  • import 只能寫在頂層,因為是靜態語法

樣例

  1. export 只支援匯出介面,可以看作物件形式,值無法被當成介面,所以是錯誤的。
/*錯誤的寫法*/
// 寫法一
export 1;

// 寫法二
var m = 1;
export m;

/*正確的四種寫法*/
// 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

// 寫法三
var n = 1;
export {n as m};

// 寫法四
var n = 1;
export default n;
複製程式碼
  1. export default 命令用於指定模組的預設輸出。export default 就是輸出一個叫做 default 的變數或方法,然後系統允許你為它取任意名字
// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同於
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同於
// import foo from 'modules';
複製程式碼

比較

JavaScript 模組規範主要有四種:CommonJS、AMD、CMD、ES Module。 CommonJS 用在伺服器端,AMD 和CMD 用在瀏覽器環境,ES Module 是作為終極通用解決方案。

AMD 和 CMD 的區別

  • 執行時機: AMD 是提前執行,CMD 是延遲執行。
  • 對依賴的處理:AMD 推崇依賴前置,CMD 推崇依賴就近。
  • API 設計理念:AMD 的 API 預設是一個當多個用,非常靈活,CMD 的 API 嚴格區分,推崇職責單一。
  • 遵循的規範:RequireJS 遵循的是 Modules/AMD 規範,SeaJS 遵循的是 Mdoules/Wrappings 規範的 define 形式。
  • 設計理念:SeaJS 設計理念是 focus on web, 努力成為瀏覽器端的模組載入器,RequireJS 想成為瀏覽器端的模組載入器,同時也想成為 Rhino / Node 等環境的模組載入器。

CommonJS 和 ES Module 的區別

  • 載入時機:CommonJS 是執行時載入(動態載入),ES Module 是編譯時載入(靜態載入)
  • 載入模組:CommonJS 模組就是物件,載入的是該物件,ES Module 模組不是物件,載入的不是物件,是介面
  • 載入結果:CommonJS 載入的是整個模組,即將所有的介面全部載入進來,ES Module 可以單獨載入其中的某個介面(方法)
  • 輸出:CommonJS 輸出值的拷貝,ES Module 輸出值的引用
  • this: CommonJS 指向當前模組,ES Module 指向 undefined

參考

CommonJS 知識

AMD 模組相關

CMD 模組相關

ES Module 模組相關

各個規範之間的比較

模組化歷史

相關文章