JavaScript 模塊化:CommonJS、AMD、CMD、UMD 和 ES6 Module

隨著 JavaScript 日新月異的發(fā)展,超過了它產(chǎn)生時候的自我定位,由于沒有模塊管理的概念,在做大型項目或文件組織的時候,就會異常糾結(jié),而且后續(xù)也很難維護,長此以往,模塊化是必然趨勢~

模塊化的主要特征是:

  • 可復(fù)用
  • 封裝了變量和函數(shù),和全局的 namaspace 不接觸,松耦合
  • 只暴露可用的 public 方法,其它私有方法全部隱藏

目前比較流行的 JS 模塊化規(guī)范有 CommonJS、AMD、CMD、UMD 以及 ES6 的模塊化。

一、CommonJS

Node.js 是 CommonJS 規(guī)范的主要實踐者,它有四個重要的環(huán)境變量為模塊化的實現(xiàn)提供支持:module、exports、require、global。實際使用時,通過 module.exports 導(dǎo)出對外的變量或接口,通過 require 導(dǎo)入其它模塊的輸出到當(dāng)前的模塊的作用域中。

模塊定義

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

module.exports = { // 在這里寫需要向外暴露的變量或函數(shù)
    basicNum: basicNum,
    add: add
};

模塊引用

// 引入自定義的模塊,參數(shù)需要包含路徑,可省略后綴.js
var math = require('./math);
math.add(3, 5);
// 引入核心模塊,參數(shù)直接寫模塊名,不需要包含路徑
var http = require('http');
http.createServer(...).listen(8080);

module.exports v.s. exports

很多時候,我們會看到在一個模塊中有兩種方式來輸出變量:
方式一:對 module.exports 賦值

// hello.js
function sayHello() {
    console.log('Hello');
}
function sayGoodbye() {
    console.log('Goodbye');
}

module.exports = {
    sayHello: sayHello,
    sayGoodbye: sayGoodbye
};

方式二:直接使用 exports

// hello.js
function sayHello() {
    console.log('Hello');
}
function sayGoodbye() {
    console.log('Goodbye');
}

exports.sayHello = sayHello;
exports.sayGoodbye = sayGoodbye;

但是,不可以直接對 exports 賦值。

// 代碼可以執(zhí)行,但是并沒有輸出任何變量
exports = {
    sayHello: sayHello,
    sayGoodbye: sayGoodbye
};

原因是什么呢?我們來分析一下 Node.js 的加載機制。
首先,Node.js 會把待加載的文件 hello.js 放入一個包裝函數(shù) load() 中執(zhí)行。在執(zhí)行 load() 函數(shù)前,Node.js 準(zhǔn)備好了 module 變量:

var module = {
    id: 'hello',
    exports: {}
};

load() 函數(shù)最終返回 module.exports

var load = function(module) {
    // hello.js 文件的內(nèi)容
    ...
    
    // load 函數(shù)返回
    return module.exports;
};

var exports = load(module);

也就是說,exports 實際上是 module.exports 的引用,或者理解為 exports 是一個指針,指向 module.exports ,所以在使用 exports 的時候,只能是 exports.sayHello = function() {...} 這樣的方式,而不能使用 exports = { sayHello: function() {}},這種方式相當(dāng)于重新定義了 exports,module.exports 仍然是空對象 {},所以給 exports 賦值是無效的。

優(yōu)點:解決了依賴、全局變量污染的問題。
缺點:CommonJS 用同步的方式加載模塊。在服務(wù)端,模塊文件都存在本地磁盤,讀取非常快,所以這樣做不會有問題。但是在瀏覽器端,限于網(wǎng)絡(luò)原因,更合理的方案是使用異步加載。

二、AMD

AMD( Asynchronous Module Definition ) 是 Require.js 在推廣過程中對模塊定義的規(guī)范化產(chǎn)出。

AMD 規(guī)范采用異步方式加載模塊,所有依賴這個模塊的語句都定義在一個回調(diào)函數(shù)中,等到加載完成后,這個回調(diào)函數(shù)才會執(zhí)行。

實現(xiàn) AMD 規(guī)范的模塊化通過 define() 方法將代碼定義為模塊,通過 require() 方法實現(xiàn)模塊的加載。

這里以 require.js 為例,首先將 require.js 引入到頁面中:

<script src="js/require.js" data-main="js/main"></script>

定義模塊

(1)獨立模塊
即 不需要依賴任何其他模塊

// math.js
define(function() {
    var basicNum = 0;
    var add = function(a, b) {
        return a + b;
    };
    return {
        basicNum: basicNum,
        add: add
    };
});

(2)非獨立模塊
即 需要依賴其他模塊

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(3, 5);
    $('#sum').html(sum);
});

require.js 還提供了一個 API: require.config() ,可以用來配置項目中用到的基礎(chǔ)模塊。

// 通過 config() 指定各模塊路徑和引用名
require.config({
    baseUrl: 'js/lib',
    paths: {
        'jquery': 'jquery.min', // 實際路徑為 js/lib/jquery.min.js
        'underscore': 'underscore.min'
    }
});

// 引入模塊
require(['jquery', 'underscore'], function($, _) {
    ...
});

優(yōu)點:適合在瀏覽器環(huán)境中異步加載模塊,可以并行加載多個模塊。
缺點:不能按需加載,開發(fā)成本大。

三、CMD

CMD( Common Module Definition ) 是 Sea.js 在推廣過程中對模塊定義的規(guī)范化產(chǎn)出。

AMD 推崇依賴前置、提前執(zhí)行,CMD 推崇依賴就近、延遲執(zhí)行。

// AMD 寫法
define(['a', 'b', 'c', 'd', 'e'], function(a, b, c, d, e) {
    // 等于在最前面聲明并初始化了所有依賴的模塊
    a.doSomething();
    if (false) {
        // 即使沒有用到某個模塊 b,但 b 還是提前執(zhí)行了
        b.doSomething();
    }
});

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

四、UMD

UMD ( Universal Module Definition ),希望提供一個前后端跨平臺的解決方案(支持 AMD 與 CommonJS 模塊方式)。

UMD 的實現(xiàn)原理:

  1. 先判斷是否支持 Node.js 模塊格式( exports 是否存在 ),存在則使用 Node.js 模塊格式。
  2. 再判斷是否支持 AMD(define 是否存在),存在則使用 AMD 方式加載模塊。
  3. 前兩個都不存在,則將模塊公開到全局( window 或 global )。

下面是一個示例:
eventUtil.js

(function(root, factory) {
    if (typef exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define(factory);
    } else {
        root.eventUtil = factory();
    }
})(this, function() {
    // module
    return {
        addEvent: function(el, type, handle) {
            // ...
        },
        removeEvent: function(el, type, handle) {
            // ...
        }
    };
});

五、ES6 Module

在 ES6 中, 我們可以通過 import 引入模塊,通過 export 導(dǎo)出模塊,功能比前幾個方案更強大,也是我們推薦使用的,但是由于瀏覽器對 ES6 的支持程度不同,目前都是使用 babel 或 traceur 把 ES6 代碼轉(zhuǎn)化為 ES5 代碼,然后再在瀏覽器環(huán)境中執(zhí)行。

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

export { basicNum, add };
// 引用模塊
import { basicNum, add } from './math';

function test(element) {
    element.textContent = add(basicNum, 99);
}
test();

導(dǎo)出模塊時還可以用 export default ,為模塊指定默認(rèn)輸出,對應(yīng)的 import 語句不需要使用大括號。

// 輸出模塊
export default {
    basicNum,
    add
}

// 引入模塊
import math from './math';

注:一個模塊只能有一個 export default。

六、CommonJS 與 ES6 模塊化的差異

1. CommonJS 支持動態(tài)導(dǎo)入,也就是 require(${path}/xx.js) ,ES6 目前還不支持,但是已有提案。

2. CommonJS 是同步導(dǎo)入,ES6是異步導(dǎo)入。

  • CommonJS 因為用于服務(wù)端,文件都在本地,同步導(dǎo)入即使卡住主線程影響也不大。
  • ES6 因為用于瀏覽器,需要下載文件,如果也采用同步導(dǎo)入會對渲染有很大影響。

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

  • CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值;另一方面,如果導(dǎo)出的值變了,導(dǎo)入的值也不會變,所以如果想更新值,必須重新導(dǎo)入一次。
  • ES6 采用實時綁定的方式,導(dǎo)入和導(dǎo)出的值都指向同一個內(nèi)存地址,所以導(dǎo)入的值會跟隨導(dǎo)出的值變化。

4. CommonJS 模塊是運行時加載,ES6 模塊是編譯時加載。

  • CommonJS 模塊就是一個對象,在導(dǎo)入時先加載整個模塊,生成一個對象( 這個對象只有在腳本運行完才會生成 ),然后再從這個對象上讀取方法,這種加載稱為“運行時加載”。
  • ES6 模塊不是對象,它的對外接口只是一種靜態(tài)定義,在代碼運行之前( 即編譯時 )的靜態(tài)解析階段就完成了模塊加載,比 CommonJS 模塊的加載方式更高效。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Javascript模塊化編程,已經(jīng)成為一個迫切的需求。理想情況下,開發(fā)者只需要實現(xiàn)核心的業(yè)務(wù)邏輯,其他都可以加載...
    zhoulujun閱讀 3,024評論 0 14
  • 模塊通常是指編程語言所提供的代碼組織機制,利用此機制可將程序拆解為獨立且通用的代碼單元。所謂模塊化主要是解決代碼分...
    MapleLeafFall閱讀 1,256評論 0 0
  • 上一章介紹了模塊的語法,本章介紹如何在瀏覽器和 Node 之中加載 ES6 模塊,以及實際開發(fā)中經(jīng)常遇到的一些問題...
    emmet7life閱讀 2,902評論 0 1
  • 模塊通常是指編程語言所提供的代碼組織機制,利用此機制可將程序拆解為獨立且通用的代碼單元。所謂模塊化主要是解決代碼分...
    一個敲代碼的前端妹子閱讀 2,026評論 8 23
  • “我是始終堅決拒斥生活的雞血狗血的,因為無論是打雞血帶來的亢奮,還是對生活做出狗血的解釋,都那么容易流于表面,...
    茅魚兒閱讀 321評論 0 1

友情鏈接更多精彩內(nèi)容