瞅瞅JavaScript模塊標(biāo)準(zhǔn)

模塊是每門(mén)語(yǔ)言構(gòu)建復(fù)雜系統(tǒng)的必備特性,JavaScript自然也不例外。JavaScript當(dāng)前流行的模塊化標(biāo)準(zhǔn)有CommonJS、AMD、CMD、ES6等等,本文對(duì)這些標(biāo)準(zhǔn)做了簡(jiǎn)單梳理,努力做到應(yīng)用時(shí)不懵逼,不亂用。

模塊

現(xiàn)如今幾乎每門(mén)語(yǔ)言都有自己的模塊化解決方案,這是隨著軟件工程越來(lái)越復(fù)雜的必然產(chǎn)物。貼幾個(gè)流行語(yǔ)言的模塊化介紹大家感受下:

所有語(yǔ)言的模塊化解決方案都是為了實(shí)現(xiàn)將復(fù)雜的程序拆分成獨(dú)立的幾個(gè)模塊,每個(gè)模塊寫(xiě)明自己的依賴(lài)、輸出自己的能力。模塊化讓復(fù)雜代碼變得容易維護(hù)、方便復(fù)用。

概覽

JavaScript標(biāo)準(zhǔn)眾多,縷清這幾個(gè)標(biāo)準(zhǔn)的發(fā)展史有助于大家選擇采用哪種方案來(lái)寫(xiě)代碼。

  1. CommonJS應(yīng)該是最早在民間自發(fā)產(chǎn)生的服務(wù)端模塊化標(biāo)準(zhǔn),一開(kāi)始是叫ServerJS,后來(lái)改名了。
  2. 服務(wù)端JS有了模塊化標(biāo)準(zhǔn)之后,瀏覽器JS表示我也必須有,于是基于CommonJS標(biāo)準(zhǔn)產(chǎn)生了AMD,和CommonJS相比最大的不同就是依賴(lài)的異步加載。
  3. CMD是類(lèi)似AMD的對(duì)于瀏覽器JS模塊化標(biāo)準(zhǔn),源自Sea.js。
  4. ES6則是集大成者,其統(tǒng)一了同步和異步的模塊化標(biāo)準(zhǔn),試圖讓JS模塊化標(biāo)準(zhǔn)從分裂走向統(tǒng)一,并取得了不小的成績(jī)。

標(biāo)準(zhǔn)定制一般都是和實(shí)現(xiàn)相輔相成的,那么JS這些有名的模塊化標(biāo)準(zhǔn)主要都有哪些實(shí)現(xiàn)呢?

CommonJS AMD CMD ES6
Node.js/RingoJS RequireJS/curl.js SeaJS ES6

每個(gè)標(biāo)準(zhǔn)都在JS世界的不同領(lǐng)域中得到廣泛的應(yīng)用,對(duì)這些標(biāo)準(zhǔn)進(jìn)行初步的了解是有必要的。

CommonJS

為了方便,直接使用Node.js的模塊化實(shí)現(xiàn)來(lái)說(shuō)明CommonJS標(biāo)準(zhǔn)。下面給出按照CommonJS標(biāo)準(zhǔn)寫(xiě)的demo,隨后其他標(biāo)準(zhǔn)的demo也會(huì)實(shí)現(xiàn)一樣的功能。

// math.js
const { PI } = Math;
exports.area = (r) => PI * r ^ 2;
exports.circumference = (r) => 2 * PI * r;
console.log(module);

// main.js
var area = require('./math').area;
var result = area(3);
console.log(result);

CommonJS模塊定義了三個(gè)變量,moduleexportsrequire

module

通過(guò)console.log(module),我們可以打印出module的結(jié)構(gòu)如下:

Module {
  id: '.',                                                      // 模塊Id,一般都是文件的絕對(duì)路徑
  exports: { area: [Function], circumference: [Function] },     // 模塊對(duì)外輸出的變量
  parent: null,                                                 // 調(diào)用該模塊的模塊,如果直接執(zhí)行就是null
  filename: '/path/to/demo/math.js',                            // 帶絕對(duì)路徑的文件名
  loaded: false,                                                // 模塊是否加載完成
  children: [],                                                 // 模塊的依賴(lài)
  paths:                                                        // 模塊依賴(lài)的搜索路徑
   [ '/path/to/demo/node_modules',
     '/path/to/node_modules',
     '/path/node_modules',
     '/node_modules' ] }

exports

module對(duì)象中是有字段exports的,exports實(shí)際上就是module.exports

var exports = module.exports;

因此導(dǎo)出變量有兩種方式:

exports.area = (r) => PI * r ^ 2;
exports.circumference = (r) => 2 * PI * r;

// 或者也可以如下
module.exports.area = (r) => PI * r ^ 2;
module.exports.circumference = (r) => 2 * PI * r;

因?yàn)?code>exports是module.exports的引用,在導(dǎo)出的時(shí)候我們就要格外小心了。

exports.area = (r) => PI * r ^ 2;
module.exports = (r) => 2 * PI * r; // 將module.exports對(duì)象覆蓋,area這個(gè)變量就不會(huì)被導(dǎo)出。

exports = (r) => 2 * PI * r; // exports就不再是module.exports的引用了,會(huì)導(dǎo)致后面的circumference導(dǎo)出無(wú)效。
exports.circumference = (r) => 2 * PI * r;

require

require的參數(shù)是模塊id,require實(shí)現(xiàn)的功能就是根據(jù)模塊id去找到對(duì)應(yīng)的依賴(lài)模塊。模塊id的變數(shù)主要在兩個(gè)方面,一個(gè)是后綴名,一個(gè)是路徑。

首先來(lái)說(shuō)后綴名,一般默認(rèn)是js的,所以我們?cè)谝蕾?lài)的以后一般不需要添加后綴名。而且找不到的話,Node.js還會(huì)嘗試添加.json.node后綴去查找。

var area = require('./math').area;

// 和上面是一樣的
var area = require('./math.js').area;

再來(lái)說(shuō)路徑,絕對(duì)路徑和相對(duì)路徑就不多說(shuō),比較好理解。

var area = require('/math').area;   // 在指定的絕對(duì)路徑查找模塊
var area = require('./math').area;  // 在相對(duì)與當(dāng)前目錄的路徑查找模塊

還有如果不是以"."、".."或者"/"開(kāi)頭的話,那就會(huì)先去核心模塊路徑找,找不到再按照module.paths指定的路徑找。

var area = require('math').area;

AMD

同樣的,本節(jié)采用RequireJS來(lái)說(shuō)明AMD標(biāo)準(zhǔn)。先上一個(gè)例子。

// math.js
define('app/math', function () {
    const { PI } = Math;
    return {
        area: function (r) {
          return PI * r ^2;math.js
        },
        circumference: function (r) {
          return 2 * PI * r;
        }
    };
});

// main1.js
define(['app/math', 'print'], function (math, print) {
    print(math.area(3));
});

// main2.js
define(function (require) {
    var math = require('./math');
    var print = require('print');
    print(math.area(3));
});

define

AMD使用define這個(gè)api來(lái)定義一個(gè)模塊,其語(yǔ)法比較簡(jiǎn)單。

define(id?, dependencies?, factory);

模塊id和依賴(lài)都是可選參數(shù),只有構(gòu)造函數(shù)是必須的。

id

AMD的模塊id和CommonJSmodule對(duì)象中的id作用是一樣的,用來(lái)唯一的指定模塊,一般是模塊的絕對(duì)路徑。雖然define函數(shù)將這個(gè)id暴露給使用者,但一般也是不填的,一些優(yōu)化工具會(huì)自動(dòng)生成絕對(duì)路徑作為id參數(shù)傳給define函數(shù)。id的定義也和CommonJS類(lèi)似,相對(duì)路徑、絕對(duì)路徑、js后綴可以省略等等。詳細(xì)的可以查看AMD模塊id的格式

dependencies

factory函數(shù)中使用到的依賴(lài)需要先在這里指明,比如示例代碼,需要指明app/mathprint,然后將他們作為factory的參數(shù)傳給函數(shù)體使用。AMD協(xié)議保證在factory函數(shù)執(zhí)行之前,能將所有的依賴(lài)都準(zhǔn)備好。

除了指明依賴(lài)之外,dependencies還有一種寫(xiě)法。這種寫(xiě)法是為了方便復(fù)用按照CommonJS規(guī)范寫(xiě)的模塊,足見(jiàn)AMD規(guī)范的良苦用心。

define(function(require, exports, module) {
    var a = require("a");
    exports.foo = function () {
        return a.bar();
    };
});

RequireJS中依賴(lài)的查找路徑是通過(guò)配置文件來(lái)指定的baseUrlpathsbundles等,這一點(diǎn)和Node.js是完全不一樣的。

AMD這個(gè)標(biāo)準(zhǔn)有個(gè)比較明顯的缺陷就是所有的依賴(lài)都必須要先執(zhí)行,這個(gè)從其接口的設(shè)計(jì)上就能看出來(lái)。如果依賴(lài)比較多的話,這個(gè)事情就比較坑爹了。

factory

這個(gè)參數(shù)名字比較有意思,叫工廠函數(shù),當(dāng)某塊被依賴(lài)的時(shí)候,這個(gè)工廠函數(shù)就會(huì)被執(zhí)行,而且即便被依賴(lài)多次,也只會(huì)執(zhí)行一次。在factory中需要導(dǎo)出變量的時(shí)候,直接return就可以了,當(dāng)然也可以使用CommonJS規(guī)范的exports。

相比較而言,AMD標(biāo)準(zhǔn)還是比較復(fù)雜的。

CMD

CMD雖然沒(méi)有CommonJSAMD出名,但是SeaJS在國(guó)內(nèi)還是比較出名,這里也捎帶提及CMD規(guī)范,不多說(shuō),來(lái)demo代碼先。

// math.js
define(function(require, exports, module) {
    const { PI } = Math;
    exports.area = function (r) {
        return PI * r ^2;math.js
    },
    exports.circumference = function (r) {
        return 2 * PI * r;
    }
});

// main1.js
define(function(require, exports, module) {
    var area = require('./math').area;
    var print = require('print');
    print(area(3));
});

上面的示例和AMD的示例雖然比較像,但是實(shí)際上CMD的規(guī)范和AMD還是不太一樣的,有自己的一些特色。

define

模塊定義和雖然和AMD一樣用的是define函數(shù),但是只支持factory一個(gè)參數(shù)。

define(factory);

factory和AMD也是類(lèi)似的,可以是函數(shù),也可以是一個(gè)object。

require && require.async

CMD除了有同步的require接口,還有異步接口require.async,這樣就解決了我們之前提到的AMD需要先把所有依賴(lài)都加載好才能執(zhí)行factory的弊端。

define(function(require) {
    // 同步接口示例
    var a = require('./a');
    a.doSomething();

    // 異步接口示例
    require.async('./b', function(b) {
        b.doSomething();
    });
})

exports

這個(gè)就比較類(lèi)似CommonJS的exports了,是用來(lái)輸出API或者對(duì)象的。

module

這個(gè)也比較類(lèi)似CommonJS的module對(duì)象,不過(guò)相比于Node.js的module對(duì)象要簡(jiǎn)單的多,只包括

module.uri                  // 模塊完整解析出來(lái)的uri
module.dependencies         // 所有的依賴(lài)
module.exports              // 導(dǎo)出的能力

從上面的簡(jiǎn)單描述可以看出,CMD想同時(shí)解決AMD和CommonJS能解決的問(wèn)題,基于AMD和CommonJS的設(shè)計(jì)做了簡(jiǎn)化優(yōu)化,同時(shí)設(shè)計(jì)了異步require的接口等。關(guān)于CMD的大量細(xì)節(jié)可以查看SeaJS官網(wǎng)

ES6

一直以來(lái)JavaScript語(yǔ)言本身是沒(méi)有內(nèi)置的模塊系統(tǒng),ES6終結(jié)了這個(gè)局面。雖然ES6的普及還需要好多年,但ES6完全兼容ES5的所有特性。ES6的寫(xiě)法可以通過(guò)轉(zhuǎn)換工具轉(zhuǎn)成ES5來(lái)執(zhí)行,是時(shí)候好好學(xué)習(xí)ES6了。

讓我們來(lái)看看用ES6實(shí)現(xiàn)上面的示例是什么樣的?

// math.js
const { PI } = Math;
export function area(r) {
    return PI * r ^ 2;
}
export function circumference(r) {
    return 2 * PI * r;
}

// main.js
import { area, circumference } from './math';
console.log(area(3));

export

ES6的模塊是嚴(yán)格要求一個(gè)模塊一個(gè)文件,一個(gè)文件一個(gè)模塊的。每個(gè)模塊可以只導(dǎo)出一個(gè)變量,也可以導(dǎo)出多個(gè)變量。

一個(gè)模塊導(dǎo)出多次使用命名的導(dǎo)出(named exports)。

export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}

一個(gè)模塊只導(dǎo)出一次使用默認(rèn)導(dǎo)出(default exports),非常方便。

export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };
export default function () {}

import

ES6的import和之前標(biāo)準(zhǔn)的require是比較不一樣的,被導(dǎo)出變量是原有變量的只讀視圖。這意味著雖然變量被導(dǎo)出了,但是它還是和內(nèi)部變量保持關(guān)聯(lián),被導(dǎo)出變量的變化,會(huì)導(dǎo)致內(nèi)部變量也跟著變化。也許這正是ES6重新取了import這個(gè)名字而沒(méi)有使用require的原因。這一點(diǎn)和require是完全不一樣的,require變量導(dǎo)出之后就生成了一個(gè)新的變量,和原始的內(nèi)部變量就脫離關(guān)系了。有個(gè)demo能比較好的說(shuō)明這個(gè)問(wèn)題。

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

模塊是ES6語(yǔ)言的一項(xiàng)重大特性,里面的細(xì)節(jié)比較多,詳細(xì)描述怕是篇幅太長(zhǎng)了,需要詳細(xì)了解ES6模塊語(yǔ)法的同學(xué)請(qǐng)移步ES Modules

總結(jié)

本文簡(jiǎn)單描述了CommonJS、AMD、CMD以及ES6的模塊標(biāo)準(zhǔn),仔細(xì)研究各個(gè)標(biāo)準(zhǔn)的細(xì)節(jié)可以一窺JavaScript模塊化標(biāo)準(zhǔn)的發(fā)展歷程。JavaScript語(yǔ)言早期作為網(wǎng)站的一種腳本語(yǔ)言,不需要模塊化這種特性,但隨著node.js的出現(xiàn),js的工程越來(lái)越復(fù)雜,模塊化也越來(lái)越重要。CommonJS、AMD和CMD是在語(yǔ)言不支持的情況下發(fā)展出來(lái)的第三方模塊化解決方案,ES6正是基于這些解決方案提出了語(yǔ)言?xún)?nèi)置的模塊標(biāo)準(zhǔn),希望ES6能盡快的推廣起來(lái),這樣JSer就能輕松許多啦。

參考文獻(xiàn)

微信一鍵關(guān)注
微信一鍵關(guān)注
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容