JavaScript 模塊化總結

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

規範JavaScript的模塊定義和加載機制,下降學習和使用各類框架的門檻,可以以一種統一的方式去定義和使用模塊,提升開發效率,下降了應用維護成本。html

目錄:前端

模塊化的歷史

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

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

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

在 1999 年的時候,那會尚未全職的前端工程師,寫 JS 是直接將變量定義在全局,作的好一些的或許會作一些文件目錄規劃,將資源歸類整理,這種方式被稱爲直接定義依賴,舉個例子:jquery

// 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>
複製代碼

可是,即便有規範的目錄結構,也不能避免由此而產生的大量全局變量,這就致使了一不當心就會有變量衝突的問題,就比如上面這個例子中的 writeHellowebpack

因而在 2002 年左右,有人提出了命名空間模式的思路,用於解決遍地的全局變量,將須要定義的部分歸屬到一個對象的屬性上,簡單修改上面的例子,就能實現這種模式:git

// 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');
}
複製代碼

不過這種方式,毫無隱私可言,本質上就是全局對象,誰均可以來訪問而且操做,一點都不安全。es6

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

// 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 模塊相關

各個規範之間的比較

模塊化歷史

相關文章
相關標籤/搜索