談?wù)凧S的那些文件模塊系統(tǒng)

學(xué)習(xí)ES6和Webpack過程中,研究了一下CommonJS,AMD, CMD, ES6這些模塊系統(tǒng)到底有什么區(qū)別。

先提結(jié)論:

  • 相同點(diǎn)是,所有的文件模塊系統(tǒng),都采用單例模式。
  • 模塊加載方面:CJS采用同步阻塞機(jī)制,而AMD采用異步回調(diào)機(jī)制,CMD可通過調(diào)用不同的require方法,采用不同的機(jī)制。
  • 模塊執(zhí)行方面:CJS,AMD都采用預(yù)先執(zhí)行,在引用模塊的同時(shí)執(zhí)行模塊代碼;CMD則采用延遲執(zhí)行,只有當(dāng)模塊中的變量或方法真正被執(zhí)行時(shí), 模塊代碼才被執(zhí)行。
  • 個(gè)人覺得服務(wù)器端使用CJS和CMD都可以,但比較偏愛CJS簡(jiǎn)潔的語(yǔ)法。瀏覽器端從性能方面考慮還是比較推薦使用AMD,盡量避免在代碼運(yùn)行過程中去做資源請(qǐng)求。
  • 特別推薦使用webpack模塊打包工具配上ES6的模塊文件系統(tǒng)語(yǔ)法,在本地進(jìn)行模塊編譯后上線。

正文:

JS本來是沒有模塊的語(yǔ)言,隨著頁(yè)面交互越來越復(fù)雜,為了避免全局變量泛濫以及方便多人合作開發(fā)等,我們開始運(yùn)用對(duì)象,模擬類,閉包等來實(shí)現(xiàn)模塊的效果,例如:

  • 用對(duì)象將方法和變量封裝起來。
var module1 = {
  _count : 0,
  m1 : function (){
    //...
  },
  m2 : function (){
    //...
  }
};
  • 用閉包封裝方法,可得到外部無(wú)法讀取的私有變量
var module1 = (function(){
  var _count = 0;
  var m1 = function(){
    //...
  };
  var m2 = function(){
    //...
  };
  return {
    m1 : m1,
    m2 : m2
  };
})();
  • 沙箱模式,聲明模塊依賴
Sandbox.modules = {};
Sandbox.modules.dom = function(box) {
    box.getElement = function() {};
    box.getStyle = function() {console.log('getStyle')};
    box.foo = 'bar';
};
new Sandbox(['dom', 'ajax'], function(box) {     //聲明依賴
    box.getElement();
})

思路:1. 將全局屬性方法的初始化函數(shù)放在Sandbox構(gòu)造函數(shù)的屬性modules中;2. 在構(gòu)造函數(shù)內(nèi)部獲取所需的屬性,賦值給this; 3. 運(yùn)行callback(this)。具體實(shí)現(xiàn)代碼可在這篇文章里搜‘沙箱模式’查看

雖然模塊化編程能較好的幫助我們處理單個(gè)文件內(nèi)的代碼結(jié)構(gòu)。但隨著單頁(yè)面應(yīng)用的出現(xiàn),單個(gè)js文件已經(jīng)無(wú)法滿足復(fù)雜交互的要求了。
而單純依賴html<script>引用的多文件間沒有依賴關(guān)系,需要根據(jù)依賴嚴(yán)格注意在頁(yè)面中的引用順序,即使這樣,還是很容易出現(xiàn)錯(cuò)誤。因此,協(xié)調(diào)不同文件之間依賴關(guān)系模塊文件系統(tǒng)就出現(xiàn)了。

CommonJS

CommonJS 是以在瀏覽器環(huán)境之外構(gòu)建 JavaScript 生態(tài)系統(tǒng)為目標(biāo)而產(chǎn)生的項(xiàng)目,比如在服務(wù)器和桌面環(huán)境中。Node.js 就是CommonJS規(guī)范的最常見的實(shí)現(xiàn)。規(guī)范其中也定義了適合服務(wù)器端使用的同步阻塞文件模塊系統(tǒng):

語(yǔ)法:

module.exports屬性表示當(dāng)前模塊對(duì)外輸出的接口,其他文件加載該模塊,實(shí)際上就是讀取module.exports變量。

//a.js 定義模塊
module.exports = 'I am from a.js!';
//main.js 引用模塊
const a = require('./a.js');
console.log(a);  //輸出'I am from a.js!'

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

var exports = module.exports;

這讓我們可以很方便的在對(duì)外輸出模塊接口時(shí),向exports對(duì)象添加方法。最終輸出的是exports對(duì)象。

//a.js 定義模塊
exports.doStuff = function() {
  console.log('doing stuff...');
}
//main.js 引用模塊
const a = require('./a.js');
a.doStuff();  //輸出'doing stuff...'

這就解釋了如果希望輸出的是非對(duì)象類型,不能直接賦值給exports,而是要賦值給module.export。這是因?yàn)?code>exports實(shí)際上是指向module.export的,修改它的值只會(huì)修改它的指針指向別的對(duì)象或數(shù)據(jù),而不能修改module.export的值。

特征:

  • 同步阻塞:CommonJS規(guī)范加載模塊是同步的,也就是說,只有加載完成,才能執(zhí)行后面的操作。這是由于服務(wù)器端的文件是存在于本地的,加載時(shí)間比較短。但在瀏覽器端需要較長(zhǎng)時(shí)間向服務(wù)器端請(qǐng)求文件的情況下,這種文件模塊系統(tǒng)并不合適。
  • 單例模式: 雖然CommonJS允許在同一個(gè)文件對(duì)同一個(gè)模塊多次引用,但獲取到的是同一個(gè)對(duì)象(類,函數(shù))。可以看看下面這個(gè)例子:
//counter.js 定義一個(gè)計(jì)數(shù)器函數(shù),有一個(gè)內(nèi)部變量i
var i = 0
function count() {
    return ++i;
}
module.exports = count;
//main.js引用counter.js
var counter1 = require('./counter')
var counter2 = require('./counter')
var counter3 = require('./counter')
console.log(counter1())
console.log(counter2())
console.log(counter3())

運(yùn)行結(jié)果:三次引用counter.js都執(zhí)行一次,分別輸出1,2,3;說明三次引用的是同一個(gè)函數(shù),操作的是同一個(gè)變量。


WX20170321-163625.png

AMD

AMD是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義"。相對(duì)于CommonJS的同步阻塞模塊系統(tǒng),更適合用于瀏覽器端。最常用的實(shí)現(xiàn)是require.js。

它采用異步方式加載模塊,模塊的加載不影響它后面語(yǔ)句的運(yùn)行。所有依賴這個(gè)模塊的語(yǔ)句,都定義在一個(gè)回調(diào)函數(shù)中,等到加載完成之后,這個(gè)回調(diào)函數(shù)才會(huì)運(yùn)行。

語(yǔ)法:

//name.js 定義模塊
define([], function(){
  return 'daisy';
});
//age.js 定義模塊
define([], function(){
  return 23;
});
//main.js 引用模塊
require(['./name', './age'], function(name, age) {
    console.log(`Hi! I am ${name}, I am ${age} years old.`);  //Hi! I am daisy, I am 23 years old.
});

AMD也采用require()語(yǔ)句加載模塊,但是不同于CommonJS,它要求兩個(gè)參數(shù):

  • 第一個(gè)參數(shù)[module],是一個(gè)數(shù)組,里面的成員就是要加載的模塊;
  • 第二個(gè)參數(shù)callback,則是加載成功之后的回調(diào)函數(shù)。回調(diào)函數(shù)的參數(shù)依次是按照數(shù)組順序依賴的模塊內(nèi)容。

同樣的,在define()語(yǔ)句定義模塊時(shí),第一個(gè)參數(shù)數(shù)組也可以用于引用其他模塊,在回調(diào)函數(shù)的參數(shù)重輸出。

特征:

  • 異步加載: 模塊代碼采用回調(diào)函數(shù)的形式執(zhí)行,頁(yè)面不需要停下等待加載完成,因此是瀏覽器端常用的模塊文件系統(tǒng)。
  • 與CommonJS一樣,引用時(shí)采用單例模式。
//counter.js 
define([], function(){
  let i = 0;
  return function() {
    return ++i;
  }
});
//main.js 
require(['./counter'], function(counter) {
    console.log(counter());   //1
    console.log(counter());   //2
    console.log(counter());   //3
});
require(['./counter'], function(counter) {
    console.log(counter());   //4
    console.log(counter());   //5
    console.log(counter());   //6
});

CMD

CMD是個(gè)人比較不熟悉的,但感覺上更像是AMD和CJS的結(jié)合延伸。CMD 推崇 as lazy as possible,sea.js是CMD常用的實(shí)現(xiàn)。

語(yǔ)法:

define({ "foo": "bar" });
define('I am a template. My name is {{name}}.');
define(function() {
      // 模塊代碼
}) ;
//也可以接受依賴
define('hello', ['jquery'], function(require, exports, module) {
  // 模塊代碼
});
//引用
define(function(require, exports) {
  // 同步獲取模塊 a 的接口
  const a = require('./a');  
  // 調(diào)用模塊 a 的方法
  a.doSomething();

 // 異步獲取模塊b的接口
  require.async('./b', function(b) {
    b.doSomething();
  });
// 異步加載多個(gè)模塊,在加載完成時(shí),執(zhí)行回調(diào)
  require.async(['./c', './d'], function(c, d) {
    c.doSomething();
    d.doSomething();
  });
});

特征:

  • 與AMD預(yù)先加載模塊不同的是,CMD提倡就近加載:在需要用到該模塊的地方才調(diào)用require()require.async()獲取模塊接口,盡量避免預(yù)先加載不需要的模塊。
  • 對(duì)于依賴的模塊,AMD 是提前執(zhí)行,CMD 是延遲執(zhí)行。不過 RequireJS 從 2.0 開始,也改成可以延遲執(zhí)行(根據(jù)寫法不同,處理方式不同。

對(duì)于提前執(zhí)行延遲執(zhí)行的理解:AMD在最初加載模塊文件時(shí),立刻執(zhí)行模塊中的方法,并將返回值賦值給回調(diào)函數(shù)的參數(shù);而CMD在調(diào)用const a = require('./a.js')后,解析路徑后只是將a.js文件中的代碼通過<scpript>插入到頁(yè)面中,并給予唯一的id。只有當(dāng)真正調(diào)用a時(shí),才會(huì)去執(zhí)行對(duì)應(yīng)id的那段js代碼。所以叫延遲執(zhí)行。

  • 同時(shí)支持同步異步兩種模塊加載方式。

ES6

千呼萬(wàn)喚ES6終于推出了模塊文件系統(tǒng)。新一代的標(biāo)準(zhǔn)的設(shè)計(jì)理念是兼容現(xiàn)有的 CommonJS 和 AMD 模塊。但目前因?yàn)檫€沒有瀏覽器支持這個(gè)功能,我們能夠看到的只是模塊到處和引用的語(yǔ)法。個(gè)人感覺依賴ES6變量結(jié)構(gòu)語(yǔ)法,這套語(yǔ)法用起來會(huì)比之前所有文件系統(tǒng)的語(yǔ)法更加順手。

語(yǔ)法

一個(gè) ES6 的模塊是一個(gè)包含了 JS 代碼的文件。ES6 里沒有所謂的 module 關(guān)鍵字。一個(gè)模塊看起來就和一個(gè)普通的腳本文件一樣,除了以下兩個(gè)區(qū)別:

  • ES6 的模塊自動(dòng)開啟嚴(yán)格模式,即使你沒有寫 'use strict'。
  • 你可以在模塊中使用 import 和 export。
//直接在希望導(dǎo)出的 function、class、var、let 或 const 前添加export關(guān)鍵字
export function detectCats(canvas, options) {
  var kittydar = new Kittydar(options);
  return kittydar.detectCats(canvas);
}
export class Kittydar {
  ... several methods doing image processing ...
}
// This helper function isn't exported.
function resizeCanvas() {
  ...
}

//直接導(dǎo)出對(duì)象集合:
export {detectCats, Kittydar};

// no `export` keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }

在另外一個(gè)文件中,我們可以導(dǎo)入這個(gè)模塊并且使用 detectCats() 函數(shù):

//引用整個(gè)模塊導(dǎo)出對(duì)象
import kitty from "kittydar.js";
kitty.detectCats();

//直接引用模塊中的某個(gè)方法或變量
import {detectCats} from "kittydar.js";

function go() {
    var canvas = document.getElementById("catpix");
    var cats = detectCats(canvas);
    drawRectangles(canvas, cats);
}

//同時(shí)引用模塊中的多個(gè)方法或變量
import {detectCats, Kittydar} from "kittydar.js";

特征:

由于在瀏覽器端仍沒有具體的實(shí)現(xiàn),很難去判斷它的特性。Jon Coppeard 正在火狐瀏覽器上實(shí)現(xiàn) ES6 的模塊。之后包括 JavaScript Loader 規(guī)范在內(nèi)的工作已經(jīng)在進(jìn)行中。HTML 中類似 <script type=module>這樣的東西之后也會(huì)和大家見面。

總結(jié):

  • 相同點(diǎn)是,所有的文件模塊系統(tǒng),都采用單例模式。
  • 模塊加載方面:CJS采用同步阻塞機(jī)制,而AMD采用異步回調(diào)機(jī)制,CMD可通過調(diào)用不同的require方法,采用不同的機(jī)制。
  • 模塊執(zhí)行方面:CJS,AMD都采用預(yù)先執(zhí)行,在引用模塊的同時(shí)執(zhí)行模塊代碼;CMD則采用延遲執(zhí)行,只有當(dāng)模塊中的變量或方法真正被執(zhí)行時(shí), 模塊代碼才被執(zhí)行。
  • 個(gè)人覺得服務(wù)器端使用CJS和CMD都可以,但比較偏愛CJS簡(jiǎn)潔的語(yǔ)法。瀏覽器端從性能方面考慮還是比較推薦使用AMD,盡量避免在代碼運(yùn)行過程中去做資源請(qǐng)求。
  • 特別推薦使用webpack模塊打包工具配上ES6的模塊文件系統(tǒng)語(yǔ)法,在本地進(jìn)行模塊編譯后上線。

參考:
CommonJS規(guī)范
阮一峰AMD
AMD和CMD的區(qū)別from玉伯
關(guān)于AMD 是提前執(zhí)行,CMD 是延遲執(zhí)行
ES6文件系統(tǒng)模塊

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

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