JS模塊化——CommonJS AMD CMD UMD ES6 Module 比較

模塊化開發

優勢

  • 模塊化開發中,一般一個文件就是一個模塊,有本身的做用域,只向外暴露特定的變量和函數,而且能夠按需加載。
  • 依賴自動加載,按需加載。
  • 提升代碼複用率,方便進行代碼的管理,使得代碼管理更加清晰、規範。
  • 減小了命名衝突,消除全局變量。
  • 目前流行的js模塊化規範有CommonJS、AMD、CMD以及ES6的模塊系統

常見模塊化規範

  • CommonJs (Node.js)
  • AMD (RequireJS)
  • CMD (SeaJS)

CommonJS(Node.js)

CommonJS是服務器模塊的規範,Node.js採用了這個規範。html

根據 CommonJS 規範,一個單獨的文件就是一個模塊,每個模塊都是一個單獨的做用域,在一個文件定義的變量(還包括函數和類),都是私有的,對其餘文件是不可見的。前端

CommonJS規範加載模塊是同步的,也就是說,只有加載完成,才能執行後面的操做。node

CommonJS 中,加載模塊使用 require 方法。該方法讀取一個文件並執行,最後返回文件內部的 exports 對象。jquery

Node.js 主要用於服務器編程,加載的模塊文件通常都已經存在本地硬盤,加載起來較快,不用考慮異步加載的方式,因此 CommonJS 的同步加載模塊規範是比較適用的。webpack

但若是是瀏覽器環境,要從服務器加載模塊,這是就必須採用異步模式。因此就有了 AMDCMD 等解決方案。git

var x = 5;
var addX = function(value) {
  return value + x;
};

module.exports.x = x;
module.exports.addX = addX;


// 也能夠改寫爲以下
module.exports = {
  x: x,
  addX: addX,
};
複製代碼
let math = require('./math.js');
console.log('math.x',math.x);
console.log('math.addX', math.addX(4));
複製代碼

AMD (RequireJS) 異步模塊定義

  • AMD = Asynchronous Module Definition,即 異步模塊定義
  • AMD 規範加載模塊是異步的,並容許函數回調,沒必要等到全部模塊都加載完成,後續操做能夠正常執行。
  • AMD 中,使用 require 獲取依賴模塊,使用 exports 導出 API
//規範 API
define(id?, dependencies?, factory);
define.amd = {};


// 定義無依賴的模塊
define({
    add: function(x,y){
        return x + y;
    }
});


// 定義有依賴的模塊
define(["alpha"], function(alpha){
    return {
        verb: function(){
            return alpha.verb() + 1;
        }
    }
});
複製代碼

異步加載和回調

require([module], callback)callback 爲模塊加載完成後的回調函數程序員

//加載 math模塊,完成以後執行回調函數
require(['math'], function(math) {
 math.add(2, 3);
});
複製代碼

RequireJS

RequireJS 是一個前端模塊化管理的工具庫,遵循 AMD 規範,RequireJS 是對 AMD 規範的闡述。es6

RequireJS 基本思想爲,經過一個函數來將全部所需的或者所依賴的模塊裝載進來,而後返回一個新的函數(模塊)。後續全部的關於新模塊的業務代碼都在這個函數內部操做。github

RequireJS 要求每一個模塊均放在獨立的文件之中,並使用 define 定義模塊,使用 require 方法調用模塊。web

按照是否有依賴其餘模塊狀況,能夠分爲 獨立模塊非獨立模塊

  • 獨立模塊,不依賴其餘模塊,直接定義
define({
    method1: function(){},
    method2: function(){}
});

//等價於
define(function() {
    return {
        method1: function(){},
        method2: function(){}
    }
});
複製代碼
  • 非獨立模塊,依賴其餘模塊
define([ 'module1', 'module2' ], function(m1, m2) {
    ...
});

//等價於
define(function(require) {
    var m1 = require('module1');
    var m2 = require('module2');
    ...
});
複製代碼
  • require 方法調用模塊
require(['foo', 'bar'], function(foo, bar) {
    foo.func();
    bar.func();
});
複製代碼

CMD (SeaJS)

CMD = Common Module Definition,即 通用模塊定義CMDSeaJS 在推廣過程當中對模塊定義的規範化產出。

CMD規範和AMD相似,都主要運行於瀏覽器端,寫法上看起來也很相似。主要是區別在於 模塊初始化時機

  • AMD中只要模塊做爲依賴時,就會加載並初始化
  • CMD中,模塊做爲依賴且被引用時纔會初始化,不然只會加載。
  • CMD 推崇依賴就近,AMD 推崇依賴前置。
  • AMDAPI 默認是一個當多個用,CMD 嚴格的區分推崇職責單一。例如,AMDrequire 分全局的和局部的。CMD裏面沒有全局的 require,提供 seajs.use() 來實現模塊系統的加載啓動。CMD 裏每一個 API 都簡單純粹。
//AMD
define(['./a','./b'], function (a, b) {
 
    //依賴一開始就寫好
    a.test();
    b.test();
});
 
//CMD
define(function (requie, exports, module) {
     
    //依賴能夠就近書寫
    var a = require('./a');
    a.test();
     
    ...
    //軟依賴
    if (status) {
        var b = requie('./b');
        b.test();
    }
});
複製代碼

Sea.js

使用Sea.js,在書寫文件時,須要遵照CMD(Common Module Definition)模塊定義規範。一個文件就是一個模塊。

用法

  • 經過 exports 暴露接口。這意味着不須要命名空間了,更不須要全局變量。這是一種完全的命名衝突解決方案。
  • 經過 require 引入依賴。這可讓依賴內置,開發者只需關心當前模塊的依賴,其餘事情 Sea.js 都會自動處理好。對模塊開發者來講,這是一種很好的 關注度分離,能讓程序員更多地享受編碼的樂趣。
  • 經過 define 定義模塊,更多詳情參考SeasJS | 極客學院

示例

例如,對於下述util.js代碼

var org = {};
org.CoolSite = {};
org.CoolSite.Utils = {};

org.CoolSite.Utils.each = function (arr) {
  // 實現代碼
};

org.CoolSite.Utils.log = function (str) {
  // 實現代碼
};
複製代碼

能夠採用SeaJS重寫爲

define(function(require, exports) {
  exports.each = function (arr) {
    // 實現代碼
  };

  exports.log = function (str) {
    // 實現代碼
  };
});
複製代碼

經過 exports 就能夠向外提供接口。經過 require('./util.js') 就能夠拿到 util.js 中經過 exports 暴露的接口。這裏的 require 能夠認爲是 Sea.js 給 JavaScript 語言增長的一個語法關鍵字,經過 require 能夠獲取其餘模塊提供的接口。

define(function(require, exports) {
  var util = require('./util.js');
  exports.init = function() {
    // 實現代碼
  };
});
複製代碼

SeaJS與RequireJS區別

兩者區別主要表如今模塊初始化時機

  • AMD(RequireJS)中只要模塊做爲依賴時,就會加載並初始化。即儘早地執行(依賴)模塊。至關於全部的require都被提早了,並且模塊執行的順序也不必定100%就是require書寫順序。

  • CMD(SeaJS)中,模塊做爲依賴且被引用時纔會初始化,不然只會加載。即只會在模塊真正須要使用的時候才初始化。模塊加載的順序是嚴格按照require書寫的順序。

amd-cmd-nodek

從規範上來講,AMD 更加簡單且嚴謹,適用性更廣,而在RequireJS強力的推進下,在國外幾乎成了事實上的異步模塊標準,各大類庫也相繼支持AMD規範。

但從SeaJS與CMD來講,也作了不少不錯東西:一、相對天然的依賴聲明風格 二、小而美的內部實現 三、貼心的外圍功能設計 四、更好的中文社區支持。

UMD

  • UMD = Universal Module Definition,即通用模塊定義。UMDAMDCommonJS的糅合。

AMD 模塊以瀏覽器第一的原則發展,異步加載模塊。 CommonJS 模塊以服務器第一原則發展,選擇同步加載。它的模塊無需包裝(unwrapped modules)。 這迫令人們又想出另外一個更通用的模式 UMD(Universal Module Definition),實現跨平臺的解決方案。

  • UMD 先判斷是否支持 Node.js 的模塊(exports)是否存在,存在則使用 Node.js 模塊模式。再判斷是否支持 AMDdefine 是否存在),存在則使用 AMD 方式加載模塊。
(function (window, factory) {
    if (typeof exports === 'object') {
     
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
     
        define(factory);
    } else {
     
        window.eventUtil = factory();
    }
})(this, function () {
    //module ...
});
複製代碼

ES6 模塊

ES6模塊和CommonJS區別

  • ES6 模塊輸出的是值的引用,輸出接口動態綁定,而 CommonJS 輸出的是值的拷貝。
  • CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

CommonJS 輸出值的拷貝

CommonJS 模塊輸出的是值的拷貝(類比於基本類型和引用類型的賦值操做)。對於基本類型,一旦輸出,模塊內部的變化影響不到這個值。對於引用類型,效果同引用類型的賦值操做。

// lib.js
var counter = 3;
var obj = {
    name: 'David'
};

function changeValue() {
    counter++;
    obj.name = 'Peter';
};

module.exports = {
    counter: counter,
    obj: obj,
    changeValue: changeValue,
};
複製代碼
// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
console.log(mod.obj.name);  //  'David'
mod.changeValue();
console.log(mod.counter);  // 3
console.log(mod.obj.name);  //  'Peter'

// Or
console.log(require('./lib').counter);  // 3
console.log(require('./lib').obj.name);  //  'Peter'
複製代碼
  • counter 是基本類型值,模塊內部值的變化不影響輸出的值變化。
  • obj 是引用類型值,模塊內部值的變化影響輸出的值變化。
  • 上述兩點區別,類比於基本類型和引用類型的賦值操做。

也能夠藉助取值函數(getter),將 counter 轉爲引用類型值,效果以下。

在類的內部,可使用 getset 關鍵字,對某個屬性設置存執函數和取值函數,攔截該屬性的存取行爲。 —— class | 阮一峯

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};
複製代碼
// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 4
複製代碼

ES6 輸出值的引用

ES6 模塊是動態關聯模塊中的值,輸出的是值得引用。原始值變了,import 加載的值也會跟着變。

ES6 模塊的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析時,遇到模塊加載命令 import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。ES6 模塊中,原始值變了,import 加載的值也會跟着變。所以,ES6 模塊是動態引用,而且不會緩存值。 —— ES6 Module 的加載實現 | 阮一峯

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

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
複製代碼

CommonJS 運行時加載 ES6靜態編譯

CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

這是由於,CommonJS 加載的是一個對象(即 module.exports 屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

ES6 模塊是編譯時輸出接口,所以有以下2個特色

  • import 命令會被 JS 引擎靜態分析,優先於模塊內的其餘內容執行
  • export 命令會有變量聲明提高的效果
import 優先執行

在文件中的任何位置引入 import 模塊都會被提早到文件頂部

// a.js
console.log('a.js')
import { foo } from './b';

// b.js
export let foo = 1;
console.log('b.js 先執行');

// 執行結果:
// b.js 先執行
// a.js
複製代碼

雖然 a 模塊中 import 引入晚於 console.log('a'),可是它被 JS 引擎經過靜態分析,提到模塊執行的最前面,優於模塊中的其餘部分的執行。

export 命令變量提高效果

因爲 importexport 是靜態執行,因此 importexport 具備變量提高效果。即 importexport 命令在模塊中的位置並不影響程序的輸出。

// a.js
import { foo } from './b';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
  console.log('bar2');
}
export function bar3() {
  console.log('bar3');
}

// b.js
export let foo = 1;
import * as a from './a';
console.log(a);

// 執行結果:
// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }
// a.js
複製代碼

a 模塊引用了 b 模塊,b 模塊也引用了 a 模塊,export 聲明的變量也是優於模塊其它內容的執行的。但具體對變量賦值須要等到執行到相應代碼的時候。

ES6模塊和CommonJS相同點

模塊不會重複執行

重複引入某個相同的模塊時,模塊只會執行一次。

循環依賴

CommonJS 模塊循環依賴

CommonJS 模塊的重要特性是加載時執行,即腳本代碼在 require 的時候,就會所有執行。一旦出現某個模塊被「循環加載」,就只輸出已經執行的部分,還未執行的部分不會輸出。

Demo 1
//a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');
複製代碼

上面代碼之中,a.js 腳本先輸出一個 done 變量,而後加載另外一個腳本文件 b.js。注意,此時 a.js 代碼就停在這裏,等待 b.js 執行完畢,再往下執行。

再看 b.js 的代碼。

//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');
複製代碼

上面代碼之中,b.js 執行到第二行,就會去加載 a.js,這時,就發生了「循環加載」。系統會 a.js 模塊對應對象的 exports 屬性取值,但是由於 a.js 尚未執行完,從 exports 屬性只能取回已經執行的部分,而不是最後的值。

a.js 已經執行的部分,只有一行。

exports.done = false;
複製代碼

所以,對於 b.js來講,它從 a.js 只輸入一個變量 done,值爲 false

而後,b.js 接着往下執行,等到所有執行完畢,再把執行權交還給 a.js。因而,a.js 接着往下執行,直到執行完畢。咱們寫一個腳本 main.js,驗證這個過程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
複製代碼

執行 main.js,運行結果以下。

$ node main.js

在 b.js 之中,a.done = false
b.js 執行完畢
在 a.js 之中,b.done = true
a.js 執行完畢
在 main.js 之中, a.done=true, b.done=true
複製代碼

上面的代碼證實了2點

  • b.js 之中,a.js 沒有執行完畢,只執行了第一行
  • main.js 執行到第二行時,不會再次執行 b.js,而是輸出緩存的 b.js 的執行結果,即它的第四行。
exports.done = true;
複製代碼

總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。

另外,因爲 CommonJS 模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼所有執行後的值,二者可能會有差別。因此,輸入變量的時候,必須很是當心。

var a = require('a'); // 安全的寫法 導入總體,保證module已經執行完成
var foo = require('a').foo; // 危險的寫法

exports.good = function (arg) {
  return a.foo('good', arg); // 使用的是 a.foo 的最新值
};

exports.bad = function (arg) {
  return foo('bad', arg); // 使用的是一個部分加載時的值
};
複製代碼

上面代碼中,若是發生循環加載,require('a').foo 的值極可能後面會被改寫,改用 require('a') 會更保險一點。

Demo 2
// a.js
console.log('a starting');
exports.done = false;
const b = require('./b');
console.log('in a, b.done =', b.done);
exports.done = true;
console.log('a done');

// b.js
console.log('b starting');
exports.done = false;
const a = require('./a');
console.log('in b, a.done =', a.done);
exports.done = true;
console.log('b done');

// node a.js
// 執行結果:
// a starting
// b starting
// in b, a.done = false
// b done
// in a, b.done = true
// a done
複製代碼

從上面的執行過程當中,能夠看到,在 CommonJS 規範中,當遇到 require() 語句時,會執行 require 模塊中的代碼,並緩存執行的結果,當下次再次加載時不會重複執行,而是直接取緩存的結果。正由於此,出現循環依賴時纔不會出現無限循環調用的狀況。

ES6 模塊循環依賴

跟 CommonJS 模塊同樣,ES6 不會再去執行重複加載的模塊,又因爲 ES6 動態輸出綁定的特性,能保證 ES6 在任什麼時候候都能獲取其它模塊當前的最新值。

動態 import()

ES6 模塊在編譯時就會靜態分析,優先於模塊內的其餘內容執行,因此致使了咱們沒法寫出像下面這樣的代碼

if(some condition) {
  import a from './a';
}else {
  import b from './b';
}

// or 
import a from (str + 'b');
複製代碼

由於編譯時靜態分析,致使了咱們沒法在條件語句或者拼接字符串模塊,由於這些都是須要在運行時才能肯定的結果在 ES6 模塊是不被容許的,因此 動態引入import() 應運而生。

import() 容許你在運行時動態地引入 ES6 模塊,想到這,你可能也想起了 require.ensure 這個語法,可是它們的用途卻大相徑庭的。

require.ensure 的出現是 webpack 的產物,它是由於瀏覽器須要一種異步的機制能夠用來異步加載模塊,從而減小初始的加載文件的體積,因此若是在服務端的話, require.ensure 就無用武之地了,由於服務端不存在異步加載模塊的狀況,模塊同步進行加載就能夠知足使用場景了。 CommonJS 模塊能夠在運行時確認模塊加載。

import() 則不一樣,它主要是爲了解決 ES6 模塊沒法在運行時肯定模塊的引用關係,因此須要引入 import()

先來看下它的用法

  • 動態的 import() 提供一個基於 PromiseAPI
  • 動態的 import() 能夠在腳本的任何地方使用 import() 接受字符串文字,能夠根據須要構造說明符
// a.js
const str = './b';
const flag = true;
if(flag) {
  import('./b').then(({foo}) => {
    console.log(foo);
  })
}
import(str).then(({foo}) => {
  console.log(foo);
})

// b.js
export const foo = 'foo';

// babel-node a.js
// 執行結果
// foo
// foo
複製代碼

固然,若是在瀏覽器端的 import() 的用途就會變得更普遍,好比 按需異步加載模塊,那麼就和 require.ensure 功能相似了。

由於是基於 Promise 的,因此若是你想要同時加載多個模塊的話,能夠是 Promise.all 進行並行異步加載。

Promise.all([
  import('./a.js'),
  import('./b.js'),
  import('./c.js'),
]).then(([a, {default: b}, {c}]) => {
    console.log('a.js is loaded dynamically');
    console.log('b.js is loaded dynamically');
    console.log('c.js is loaded dynamically');
});
複製代碼

還有 Promise.race 方法,它檢查哪一個 Promise 被首先 resolvedreject。咱們可使用 import() 來檢查哪一個 CDN 速度更快:

const CDNs = [
  {
    name: 'jQuery.com',
    url: 'https://code.jquery.com/jquery-3.1.1.min.js'
  },
  {
    name: 'googleapis.com',
    url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js'
  }
];

console.log(`------`);
console.log(`jQuery is: ${window.jQuery}`);

Promise.race([
  import(CDNs[0].url).then(()=>console.log(CDNs[0].name, 'loaded')),
  import(CDNs[1].url).then(()=>console.log(CDNs[1].name, 'loaded'))
]).then(()=> {
  console.log(`jQuery version: ${window.jQuery.fn.jquery}`);
});
複製代碼

固然,若是你以爲這樣寫還不夠優雅,也能夠結合 async/await 語法糖來使用。

async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
複製代碼

動態 import() 爲咱們提供了以異步方式使用 ES 模塊的額外功能。

根據咱們的需求動態或有條件地加載它們,這使咱們可以更快,更好地建立更多優點應用程序。

webpack中加載3種模塊 | 語法

Webpack容許使用不一樣的模塊類型,可是底層必須使用同一種實現。全部的模塊能夠直接在盒外運行。

  • ES6 模塊
import MyModule from './MyModule.js';
複製代碼
  • CommonJS(Require)
var MyModule = require('./MyModule.js');
複製代碼
  • AMD
define(['./MyModule.js'], function (MyModule) {
});
複製代碼

參考資料

相關文章
相關標籤/搜索