1 常見的模塊化規(guī)范
- CommonJs (Node.js)
- AMD (RequireJS)
- CMD (SeaJS)
- ES Module(ES6 模塊)
2 模塊化的優(yōu)點(diǎn)
- 在模塊化開發(fā)中,通常一個文件就是一個模塊。該文件有自己的作用域,只向外暴露特定的變量和函數(shù),并且可以按需加載。
- 依賴自動加載,按需加載。
- 提高代碼復(fù)用率,方便進(jìn)行代碼的管理,使得代碼管理更加清晰、規(guī)范。
- 減少了命名沖突,消除全局變量。
- 目前流行的 js 模塊化規(guī)范有 CommonJS、AMD、CMD 以及 ES6 的模塊系統(tǒng)。
3 CommonJS(Node.js)同步加載模塊
CommonJS 是服務(wù)器模塊的規(guī)范,Node.js采用了這個規(guī)范。
根據(jù) CommonJS 規(guī)范,一個單獨(dú)的文件就是一個模塊,每個模塊都是一個單獨(dú)的作用域,每個文件中定義的變量(還包括函數(shù)和類),都是私有的,對其他文件是不可見的。
CommonJS 規(guī)范加載模塊是同步的,也就是說,只有加載完成,才能執(zhí)行后面的操作。
在 CommonJS 中,加載模塊要使用 require 方法。該方法讀取一個文件并執(zhí)行,最后返回文件內(nèi)部的 exports 對象。
math.js
var x = 5;
var addX = function(value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
// 也可以改寫為如下
module.exports = {
x: x,
addX: addX,
};
main.js
let math = require('./math.js');
console.log('math.x',math.x);
console.log('math.addX', math.addX(4));
Node.js 主要用于服務(wù)器編程,加載的模塊文件一般都已經(jīng)存在于本地硬盤中,加載起來較快,不用考慮異步加載的方式,故 CommonJS 的同步加載模塊規(guī)范是比較適用的。
但若在瀏覽器環(huán)境中,要從服務(wù)器加載模塊,這時就必須采用異步加載模式,故有了 AMD,CMD 等解決方案。
4 AMD(RequireJS)異步加載模塊
-
AMD=Asynchronous Module Definition,即 異步模塊定義。 -
AMD規(guī)范加載模塊是異步的,并允許函數(shù)回調(diào)。不必等到所有模塊都加載完成,后續(xù)操作也可正常執(zhí)行。 -
AMD中,使用require獲取依賴模塊,使用exports導(dǎo)出API。
// 規(guī)范 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;
}
}
});
異步加載和回調(diào)
require([module], callback) 中 callback 為模塊加載完成后的回調(diào)函數(shù)。
// 加載 math模塊,完成之后執(zhí)行回調(diào)函數(shù)
require(['math'], function(math) {
math.add(2, 3);
});
RequireJS
RequireJS 是一個 前端模塊化管理 的工具庫,遵循
AMD規(guī)范,RequireJS是對AMD規(guī)范的闡述。
RequireJS 的基本思想為,通過一個函數(shù)將所需或所依賴的模塊全部裝載進(jìn)來,然后返回一個新的函數(shù)(模塊)。后續(xù)所有關(guān)于新模塊的業(yè)務(wù)代碼,都在這個函數(shù)內(nèi)部操作。
RequireJS 要求每個模塊均放在獨(dú)立的文件之中,并使用 define 定義模塊,使用 require 方法調(diào)用模塊。
根據(jù)是否有依賴其他模塊的情況,可分為 獨(dú)立模塊 和 非獨(dú)立模塊。
獨(dú)立模塊(不依賴其他模塊,直接定義)
define({
method1: function(){},
method2: function(){}
});
// 等價于
define(function() {
return {
method1: function(){},
method2: function(){}
}
});
非獨(dú)立模塊(依賴其他模塊)
define([ 'module1', 'module2' ], function(m1, m2) {
...
});
// 等價于
define(function(require) {
var m1 = require('module1');
var m2 = require('module2');
...
});
調(diào)用模塊(使用 require 方法)
require(['foo', 'bar'], function(foo, bar) {
foo.func();
bar.func();
});
5 CMD(SeaJS)異步模塊
CMD 的定義
-
CMD=Common Module Definition,即 通用模塊定義。CMD是SeaJS在推廣過程中對模塊定義的規(guī)范化產(chǎn)出。 -
CMD規(guī)范和AMD規(guī)范類似,都主要運(yùn)行于瀏覽器端,寫法上看起來也很類似。主要區(qū)別,在于 模塊初始化時機(jī)。
AMD 與 CMD 的異同
-
AMD中,只要模塊作為依賴時就會加載并進(jìn)行初始化。 -
CMD中,模塊作為依賴且被引用時才會初始化,否則只會加載。 -
CMD推崇依賴就近,可以把依賴寫進(jìn)你的代碼中的任意一行。AMD推崇依賴前置。 -
AMD的API默認(rèn)是一個當(dāng)多個用,CMD嚴(yán)格的區(qū)分推崇職責(zé)單一。例如:AMD里require分全局的和局部的。CMD里面沒有全局的require,提供seajs.use()來實(shí)現(xiàn)模塊系統(tǒng)的加載啟動。CMD里每個API都簡單純粹。
// AMD
define(['./a','./b'], function (a, b) {
// 依賴一開始就寫好
a.test();
b.test();
});
// CMD
define(function (require, exports, module) {
// 依賴可以就近書寫
var a = require('./a');
a.test();
...
// 軟依賴
if (status) {
var b = require('./b');
b.test();
}
});
Sea.js
使用 Sea.js,在書寫文件時需要遵守 CMD(Common Module Definition)模塊定義規(guī)范,一個文件就是一個模塊。
用法
- 通過
exports暴露接口。這意味著不需要命名空間了,更不需要全局變量。這是一種徹底的命名沖突解決方案。 - 通過
require引入依賴。這可以讓依賴內(nèi)置,開發(fā)者只需關(guān)心當(dāng)前模塊的依賴,其他事情Sea.js都會自動處理好。對模塊開發(fā)者來說,這是一種很好的 關(guān)注度分離,能讓程序員更多地享受編碼的樂趣。 - 通過
define定義模塊。
示例
例如,對于下述 util.js 代碼
var org = {};
org.CoolSite = {};
org.CoolSite.Utils = {};
org.CoolSite.Utils.each = function (arr) {
// 實(shí)現(xiàn)代碼
};
org.CoolSite.Utils.log = function (str) {
// 實(shí)現(xiàn)代碼
};
可以采用 SeaJS 重寫為
define(function(require, exports) {
exports.each = function (arr) {
// 實(shí)現(xiàn)代碼
};
exports.log = function (str) {
// 實(shí)現(xiàn)代碼
};
});
通過 exports 就可以向外提供接口。通過 require('./util.js') 就可以拿到 util.js 中通過 exports 暴露的接口。這里的 require 可以認(rèn)為是 Sea.js 給 JavaScript 語言增加的一個語法關(guān)鍵字,通過 require 可以獲取其他模塊提供的接口。
define(function(require, exports) {
var util = require('./util.js');
exports.init = function() {
// 實(shí)現(xiàn)代碼
};
});
SeaJS 與 RequireJS 區(qū)別
二者區(qū)別主要表現(xiàn)在 模塊初始化時機(jī)
AMD(RequireJS)中只要模塊作為依賴時,就會加載并初始化。即盡早地執(zhí)行(依賴)模塊。相當(dāng)于所有的 require 都被提前了,而且模塊的執(zhí)行的順序也不一定就是 require 的書寫順序。
CMD(SeaJS)中,模塊作為依賴且被引用時才會初始化,否則只會加載。即只會在模塊真正需要使用的時候才初始化。模塊加載的順序是嚴(yán)格按照 require 書寫的順序來的。
| 規(guī)范 | 生態(tài) | 書寫 | 易于實(shí)現(xiàn) | NodeJS 模塊相似性 |
|---|---|---|---|---|
| AMD | ★★★★★ | ★★★ | ★★★★★ | ★★★ |
| CMD | ★★ | ★★★★★ | ★★★ | ★★★★★ |
從規(guī)范上來說,AMD 更加簡單且嚴(yán)謹(jǐn),適用性更廣,而在 RequireJS 強(qiáng)力的推動下,在國外幾乎成了事實(shí)上的異步模塊標(biāo)準(zhǔn),各大類庫也相繼支持 AMD 規(guī)范。
但從 SeaJS 與 CMD 來說,也做了很多不錯東西:1. 相對自然的依賴聲明風(fēng)格 2. 小而美的內(nèi)部實(shí)現(xiàn) 3. 貼心的外圍功能設(shè)計(jì) 4. 更好的中文社區(qū)支持。
6 UMD
-
UMD=Universal Module Definition,即 通用模塊定義,它是AMD和CommonJS的糅合。 -
AMD模塊以 瀏覽器第一的原則 發(fā)展,選擇異步加載。CommonJS模塊以 服務(wù)器第一原則 發(fā)展,選擇同步加載。由此,迫使人們又想出另一個更通用的模式UMD(Universal Module Definition),實(shí)現(xiàn)跨平臺的解決方案。 -
UMD先判斷支持Node.js的模塊(exports)是否存在,存在則使用Node.js模塊模式。再判斷支持AMD(define)是否存在,存在則使用AMD方式加載模塊。
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.utilName = factory());
}(this, function () {
//module ...
});
7 ES Module(ES6 模塊)
CommonJS 和 ES6 模塊 的區(qū)別
-
CommonJS模塊是 運(yùn)行時加載,ES6 模塊是 編譯時輸出接口。 - ES6 模塊 輸出的是值的引用,輸出接口動態(tài)綁定,而
CommonJS輸出的是值的拷貝。
CommonJS 輸出值的拷貝
CommonJS 模塊輸出的是值的拷貝,導(dǎo)出值改變不會導(dǎo)致導(dǎo)入值改變。
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'
// But
console.log(require('./lib').counter); // 3
console.log(require('./lib').obj.name); // 'Peter'
-
counter是基本類型值,模塊內(nèi)部值的變化 不影響 輸出的值變化。 -
obj是引用類型值,模塊內(nèi)部值的變化 會影響 輸出的值變化。 - 上述兩點(diǎn)區(qū)別,可類比基本類型和引用類型的賦值操作。
也可以借助取值函數(shù)(getter),將 counter 轉(zhuǎn)為引用類型值,效果如下。
在類的內(nèi)部,可以使用
get和set關(guān)鍵字,對某個屬性設(shè)置存值函數(shù)和取值函數(shù),攔截該屬性的存取行為。 —— 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 模塊是動態(tài)關(guān)聯(lián)模塊中的值,輸出的是值的引用。原始值變了,import 加載的值也會跟著變。
ES6模塊的運(yùn)行機(jī)制與CommonJS不一樣。JS 引擎對腳本靜態(tài)分析時,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執(zhí)行時,再根據(jù)這個只讀引用,到被加載的那個模塊里面去取值。ES6 模塊中,原始值變了,import加載的值也會跟著變。因此,ES6 模塊是動態(tài)引用,并且不會緩存值。 —— ES6 Module 的加載實(shí)現(xiàn) | 阮一峰
// 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 運(yùn)行時加載 ES6靜態(tài)編譯
CommonJS 模塊是運(yùn)行時加載,ES6 模塊是編譯時輸出接口。
這是因?yàn)椋?strong>CommonJS 加載的是一個對象(即 module.exports 屬性),該對象只有在腳本運(yùn)行完才會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態(tài)定義,在代碼靜態(tài)解析階段就會生成。
ES6 模塊是編譯時輸出接口,因此有如下2個特點(diǎn)
-
import命令會被 JS 引擎靜態(tài)分析,優(yōu)先于模塊內(nèi)的其他內(nèi)容執(zhí)行 -
export命令會有變量聲明提升的效果
import 優(yōu)先執(zhí)行
在文件中的任何位置引入 import 模塊都會被提前到文件頂部
// a.js
console.log('a.js')
import { foo } from './b';
// b.js
export let foo = 1;
console.log('b.js 先執(zhí)行');
// 執(zhí)行結(jié)果:
// b.js 先執(zhí)行
// a.js
雖然 a 模塊中 import 引入晚于 console.log('a'),但是它被 JS 引擎通過靜態(tài)分析,提到模塊執(zhí)行的最前面,優(yōu)于模塊中的其他部分的執(zhí)行。
export 命令變量提升效果
由于 import 和 export 是靜態(tài)執(zhí)行,所以 import 和 export 具有變量提升效果。即 import 和 export 命令在模塊中的位置并不影響程序的輸出。
// 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);
// 執(zhí)行結(jié)果:
// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }
// a.js
a 模塊引用了 b 模塊,b 模塊也引用了 a 模塊,export 聲明的變量也是優(yōu)于模塊其它內(nèi)容的執(zhí)行的。但具體對變量賦值需要等到執(zhí)行到相應(yīng)代碼的時候。
ES6 模塊和 CommonJS 相同點(diǎn)
模塊不會重復(fù)執(zhí)行
重復(fù)引入某個相同的模塊時,模塊只會執(zhí)行一次。
循環(huán)依賴
CommonJS 模塊循環(huán)依賴
CommonJS 模塊的重要特性是加載時執(zhí)行,即腳本代碼在 require 的時候,就會全部執(zhí)行。一旦出現(xiàn)某個模塊被“循環(huán)加載”,就只輸出已經(jīng)執(zhí)行的部分,還未執(zhí)行的部分不會輸出。
Demo 1
//a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done =', b.done);
exports.done = true;
console.log('a.js 執(zhí)行完畢');
上面代碼之中,a.js 腳本先輸出一個 done 變量,然后加載另一個腳本文件 b.js。注意,此時 a.js 代碼就停在這里,等待 b.js 執(zhí)行完畢,再往下執(zhí)行。
再看 b.js 的代碼。
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done =', a.done);
exports.done = true;
console.log('b.js 執(zhí)行完畢');
上面代碼之中,b.js 執(zhí)行到第二行,就會去加載 a.js,這時,就發(fā)生了“循環(huán)加載”。系統(tǒng)會在 a.js 模塊對應(yīng)對象的 exports 屬性取值。可是因?yàn)?a.js 還沒有執(zhí)行完,從 exports 屬性只能取回已經(jīng)執(zhí)行的部分,而不是最后的值。
a.js 已經(jīng)執(zhí)行的部分,只有一行。
exports.done = false;
因此,對于 b.js來說,它從 a.js 只輸入一個變量 done,值為 false。
然后,b.js 接著往下執(zhí)行,等到全部執(zhí)行完畢,再把執(zhí)行權(quán)交還給 a.js。于是,a.js 接著往下執(zhí)行,直到執(zhí)行完畢。我們寫一個腳本 main.js,驗(yàn)證這個過程。
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=, b.done=', a.done, b.done);
執(zhí)行 main.js,運(yùn)行結(jié)果如下。
$ node main.js
在 b.js 之中,a.done = false
b.js 執(zhí)行完畢
在 a.js 之中,b.done = true
a.js 執(zhí)行完畢
在 main.js 之中, a.done = true, b.done = true
上面的代碼證明了2點(diǎn)
- 在
b.js之中,a.js沒有執(zhí)行完畢,只執(zhí)行了第一行。 -
main.js執(zhí)行到第二行時,不會再次執(zhí)行b.js,而是輸出緩存的b.js的執(zhí)行結(jié)果,即它的第四行。
exports.done = true;
總之,CommonJS 輸入的是被輸出值的拷貝,不是引用。
另外,由于 CommonJS 模塊遇到循環(huán)加載時,返回的是當(dāng)前已經(jīng)執(zhí)行的部分的值,而不是代碼全部執(zhí)行后的值,兩者可能會有差異。所以,輸入變量的時候,必須非常小心。
var a = require('a'); // 安全的寫法 導(dǎo)入整體,保證 module 已經(jīng)執(zhí)行完成
var foo = require('a').foo; // 危險(xiǎn)的寫法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一個部分加載時的值
};
上面代碼中,如果發(fā)生循環(huán)加載,require('a').foo 的值很可能后面會被改寫,改用 require('a') 會更保險(xiǎn)一點(diǎn)。
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
// 執(zhí)行結(jié)果:
// a starting
// b starting
// in b, a.done = false
// b done
// in a, b.done = true
// a done
從上面的執(zhí)行過程中,可以看到,在 CommonJS 規(guī)范中,當(dāng)遇到 require() 語句時,會執(zhí)行 require 模塊中的代碼,并緩存執(zhí)行的結(jié)果,當(dāng)下次再次加載時不會重復(fù)執(zhí)行,而是直接取緩存的結(jié)果。正因?yàn)榇?,出現(xiàn)循環(huán)依賴時才不會出現(xiàn)無限循環(huán)調(diào)用的情況。
ES6 模塊循環(huán)依賴
跟 CommonJS 模塊一樣,ES6 不會再去執(zhí)行重復(fù)加載的模塊,又由于 ES6 動態(tài)輸出綁定的特性,能保證 ES6 在任何時候都能獲取其它模塊當(dāng)前的最新值。
ES6 動態(tài) import()
ES6 模塊在編譯時就會靜態(tài)分析,優(yōu)先于模塊內(nèi)的其他內(nèi)容執(zhí)行,所以導(dǎo)致了我們無法寫出像下面這樣的代碼
if(some condition) {
import a from './a';
}else {
import b from './b';
}
// or
import a from (str + 'b');
因?yàn)榫幾g時靜態(tài)分析,導(dǎo)致了我們在引用的時候,無法使用 條件語句 或者 拼接字符串模塊,因?yàn)檫@些都是需要在運(yùn)行時才能確定的結(jié)果在 ES6 模塊是不被允許的,所以 動態(tài)引入import() 應(yīng)運(yùn)而生。
import() 允許你在運(yùn)行時動態(tài)地引入 ES6 模塊,想到這,你可能也想起了 require.ensure 這個語法,但是它們的用途卻截然不同的。
require.ensure 的出現(xiàn)是 webpack 的產(chǎn)物,它是因?yàn)闉g覽器需要一種異步的機(jī)制可以用來異步加載模塊,從而減少初始的加載文件的體積,所以如果在服務(wù)端的話, require.ensure 就無用武之地了,因?yàn)榉?wù)端不存在異步加載模塊的情況,模塊同步進(jìn)行加載就可以滿足使用場景了。 CommonJS 模塊可以在運(yùn)行時確認(rèn)模塊加載。
而 import() 則不同,它主要是為了解決 ES6 模塊無法在運(yùn)行時確定模塊的引用關(guān)系,所以需要引入 import()。
先來看下它的用法
- 動態(tài)的
import()提供一個基于Promise的API - 動態(tài)的
import()可以在腳本的任何地方使用import()接受字符串文字,可以根據(jù)需要構(gòu)造說明符
// 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
// 執(zhí)行結(jié)果
// foo
// foo
當(dāng)然,如果在瀏覽器端的 import() 的用途就會變得更廣泛,比如 按需異步加載模塊,那么就和 require.ensure 功能類似了。
因?yàn)槭腔?Promise 的,所以如果你想要同時加載多個模塊的話,可以是 Promise.all 進(jìn)行并行異步加載。
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 被首先 resolved 或 reject。我們可以使用 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}`);
});
當(dāng)然,如果你覺得這樣寫還不夠優(yōu)雅,也可以結(jié)合 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'),
]);
}
動態(tài) import() 為我們提供了以異步方式使用 ES 模塊的額外功能。
根據(jù)我們的需求動態(tài)或有條件地加載它們,這使我們能夠更快,更好地創(chuàng)建更多優(yōu)勢應(yīng)用程序。
8 webpack中加載3種模塊 | 語法
Webpack 允許使用不同的模塊類型,但是底層必須使用同一種實(shí)現(xiàn)。所有的模塊可以直接在盒外運(yùn)行。
- ES6 模塊
import MyModule from './MyModule.js';
- CommonJS(require)
var MyModule = require('./MyModule.js');
- AMD
define(['./MyModule.js'], function (MyModule) {});