談?wù)勄岸四K化的演變歷程
隨著前端項(xiàng)目越來(lái)越大,代碼復(fù)雜性不斷增加,對(duì)于模塊化的需求越來(lái)越大。模塊化是工程化基礎(chǔ),只有將代碼模塊化,拆分為合理單元,才具備調(diào)度整合的能力。下面就來(lái)看看模塊化的概念,以及不同模塊化方案的使用方式和優(yōu)缺點(diǎn)。
1、模塊概述
由于代碼之間會(huì)發(fā)生大量交互,如果結(jié)構(gòu)不合理,這些代碼就會(huì)變得難以維護(hù)、難以測(cè)試、難以調(diào)試。而使用模塊化就解決了這些問(wèn)題,模塊化的特點(diǎn)如下:
可重用性: 當(dāng)應(yīng)用被組織成模塊時(shí),可以方便的在其他地方重用這些模塊,避免編寫(xiě)重復(fù)代碼,從而加快開(kāi)發(fā)流程;
可讀性: 當(dāng)應(yīng)用變得越來(lái)越復(fù)雜時(shí),如果在一個(gè)文件中編寫(xiě)所有功能,代碼會(huì)變得難以閱讀。如果使用模塊設(shè)計(jì)應(yīng)用,每個(gè)功能都分布在各自的模塊中,代碼就會(huì)更加清晰、易讀;
可維護(hù)性: 軟件的美妙之處在于進(jìn)化,從長(zhǎng)遠(yuǎn)來(lái)看,我們需要不斷為應(yīng)用增加新的功能。當(dāng)應(yīng)用被結(jié)構(gòu)化為模塊時(shí),可以輕松添加或刪除功能。除此之外,修復(fù)錯(cuò)誤也是軟件維護(hù)的一部分,使用模塊就可以更快速地定位問(wèn)題。
模塊化是一種將系統(tǒng)分離成獨(dú)立功能部分的方法,可以將系統(tǒng)分割成獨(dú)立的功能部分,嚴(yán)格定義模塊接口,模塊間具有透明性。通過(guò)將代碼進(jìn)行模塊化分隔,每個(gè)文件彼此獨(dú)立,開(kāi)發(fā)者更容易開(kāi)發(fā)和維護(hù)代碼,模塊之間又能夠互相調(diào)用和通信,這就是現(xiàn)代化開(kāi)發(fā)的基本模式。
(2)模式
JavaScript 模塊包含三個(gè)部分:
- 導(dǎo)入: 在使用模塊時(shí),需要將所需模塊作為依賴(lài)項(xiàng)導(dǎo)入。例如,如果想要?jiǎng)?chuàng)建一個(gè) React 組件,就需導(dǎo)入 react 模塊。要使用像 Lodash 這樣的工具庫(kù),就需要安裝并導(dǎo)入它作為依賴(lài)項(xiàng);
- 代碼: 模塊具體代碼;
- 導(dǎo)出: 模塊接口,從模塊中導(dǎo)出的內(nèi)容可供導(dǎo)入模塊的任何地方使用。
(3)類(lèi)型
模塊化的貫徹執(zhí)行離不開(kāi)相應(yīng)的約定,即規(guī)范。這是能夠進(jìn)行模塊化工作的重中之重。實(shí)現(xiàn)模塊化的規(guī)范有很多,比如:AMD、RequireJS、CMD、SeaJS、UMD、CommonJS、ES6 Module。除此之外,IIFE(立即執(zhí)行函數(shù))也是實(shí)現(xiàn)模塊化的一種方案。
本文將介紹其中的六個(gè):
- IIFE: 立即調(diào)用函數(shù)表達(dá)式
- AMD: 異步模塊加載機(jī)制
- CMD: 通用模塊定義
- UMD: 統(tǒng)一模塊定義
- CommonJS: Node.js 采用該規(guī)范
- ES 模塊: JavaScript 內(nèi)置模塊系統(tǒng)
2. IIFE
在 ECMAScript 6 之前,模塊并沒(méi)有被內(nèi)置到 JavaScript 中,因?yàn)?JavaScript 最初是為小型瀏覽器腳本設(shè)計(jì)的。這種模塊化的缺乏,導(dǎo)致在代碼的不同部分使用了共享全局變量。
比如,對(duì)于以下代碼:
var name = 'JavaScript';
var age = 20;
當(dāng)上面的代碼運(yùn)行時(shí),name 和 age 變量會(huì)被添加到全局對(duì)象中。因此,應(yīng)用中的所有 JavaScript 腳本都可以訪問(wèn)全局變量 name 和 age,這就很容易導(dǎo)致代碼錯(cuò)誤,因?yàn)樵谄渌幌嚓P(guān)的單元中也可以訪問(wèn)和修改這些全局變量。除此之外,向全局對(duì)象添加變量會(huì)使全局命名空間變得混亂并增加了命名沖突的機(jī)會(huì)。
所以,我們就需要一種封裝變量和函數(shù)的方法,并且只對(duì)外公開(kāi)定義的接口。因此,為了實(shí)現(xiàn)模塊化并避免使用全局變量,可以使用如下方式來(lái)創(chuàng)建模塊:
(function () {
// 聲明私有變量和函數(shù)
return {
// 聲明公共變量和函數(shù)
}
})();
上面的代碼就是一個(gè)返回對(duì)象的閉包,這就是我們常說(shuō)的IIFE(Immediately Invoked Function Expression),即立即調(diào)用函數(shù)表達(dá)式。在該函數(shù)中,就創(chuàng)建了一個(gè)局部范圍。這樣就避免了使用全局變量(IIFE 是匿名函數(shù)),并且代碼單元被封裝和隔離。
可以這樣來(lái)使用 IIFE 作為一個(gè)模塊:
var module = (function(){
var age = 20;
var name = 'JavaScript'
var fn1 = function(){
console.log(name, age)
};
var fn2 = function(a, b){
console.log(a + b)
};
return {
age,
fn1,
fn2,
};
})();
module.age; // 20
module.fn1(); // JavaScript 20
module.fn2(128, 64); // 192
在這段代碼中,module 就是我們定義的一個(gè)模塊,它里面定義了兩個(gè)私有變量 age 和 name,同時(shí)定義了兩個(gè)方法 fn1 和 fn2,其中 fn1 中使用 module 中定義的私有變量,fn2 接收外部傳入?yún)?shù)。最后,module 向外部暴露了age、fn1、fn2。這樣就形成了一個(gè)模塊。
當(dāng)試圖在 module 外部直接調(diào)用fn1時(shí),就會(huì)報(bào)錯(cuò):
fn1(); // Uncaught ReferenceError: fn1 is not defined
當(dāng)試圖在 module 外部打印其內(nèi)部的私有變量name時(shí),得到的結(jié)果是 undefined:
module.name; // undefined
上面的 IIFE 的例子是遵循模塊模式的,具備其中的三部分,其中 age、name、fn1、fn2 就是模塊內(nèi)部的代碼實(shí)現(xiàn),返回的 age、fn1、fn2 就是導(dǎo)出的內(nèi)容,即接口。調(diào)用 module 方法和變量就是導(dǎo)入使用。
3. CommonJS
(1)概念
① 定義
CommonJS 是社區(qū)提出的一種 JavaScript 模塊化規(guī)范,它是為瀏覽器之外的 JavaScript 運(yùn)行環(huán)境提供的模塊規(guī)范,Node.js 就采用了這個(gè)規(guī)范。
注意:
- 瀏覽器不支持使用 CommonJS 規(guī)范;
- Node.js 不僅支持使用 CommonJS 來(lái)實(shí)現(xiàn)模塊,還支持最新的 ES 模塊。
CommonJS 規(guī)范加載模塊是同步的,只有加載完成才能繼續(xù)執(zhí)行后面的操作。不過(guò)由于 Node.js 主要運(yùn)行在服務(wù)端,而所需加載的模塊文件一般保存在本地硬盤(pán),所以加載比較快,而無(wú)需考慮使用異步的方式。
② 語(yǔ)法
CommonJS 規(guī)范規(guī)定每個(gè)文件就是一個(gè)模塊,有獨(dú)立的作用域,對(duì)于其他模塊不可見(jiàn),這樣就不會(huì)污染全局作用域。在 CommonJS 中,可以分別使用 export 和 require 來(lái)導(dǎo)出和導(dǎo)入模塊。在每個(gè)模塊內(nèi)部,都有一個(gè) module 對(duì)象,表示當(dāng)前模塊。通過(guò)它來(lái)導(dǎo)出 API,它有以下屬性:
- exports:模塊導(dǎo)出值。
- filename:模塊文件名,使用絕對(duì)路徑;
- id:模塊識(shí)別符,通常是使用絕對(duì)路徑的模塊文件名;
- loaded:布爾值,表示模塊是否已經(jīng)完成加載;
- parent:對(duì)象,表示調(diào)用該模塊的模塊;
- children:數(shù)組,表示該模塊要用到的其他模塊;
③ 特點(diǎn)
CommonJS 規(guī)范具有以下特點(diǎn):
- 文件即模塊,文件內(nèi)所有代碼都運(yùn)行在獨(dú)立的作用域,因此不會(huì)污染全局空間;
- 模塊可以被多次引用、加載。第一次被加載時(shí),會(huì)被緩存,之后都從緩存中直接讀取結(jié)果。
- 加載某個(gè)模塊,就是引入該模塊的 module.exports 屬性,該屬性輸出的是值拷貝,一旦這個(gè)值被輸出,模塊內(nèi)再發(fā)生變化不會(huì)影響到輸出的值。
- 模塊加載順序按照代碼引入的順序。
④ 優(yōu)缺點(diǎn)
CommonJS 的優(yōu)點(diǎn):
- 使用簡(jiǎn)單
- 很多工具系統(tǒng)和包都是使用 CommonJS 構(gòu)建的;
- 在 Node.js 中使用,Node.js 是流行的 JavaScript 運(yùn)行時(shí)環(huán)境。
CommonJS 的缺點(diǎn)
- 可以在 JavaScript 文件中包含一個(gè)模塊;
- 如果想在 Web 瀏覽器中使用它,則需要額外的工具;
- 本質(zhì)上是同步的,在某些情況下不適合在 Web 瀏覽器中使用。
(2)使用
在 CommonJS 中,可以通過(guò) require 函數(shù)來(lái)導(dǎo)入模塊,它會(huì)讀取、執(zhí)行 JavaScript 文件,并返回該模塊的 exports 對(duì)象,該對(duì)象只有在模塊腳本運(yùn)行完才會(huì)生成。
① 模塊導(dǎo)出
可以通過(guò)以下兩種方式來(lái)導(dǎo)出模塊內(nèi)容:
module.exports.TestModule = function() {
console.log('exports');
}
exports.TestModule = function() {
console.log('exports');
}
則合兩種方式的導(dǎo)出結(jié)果是一樣的,module.exports和exports的區(qū)別可以理解為:exports是module.exports的引用,如果在exports調(diào)用之前調(diào)用了exports=...,那么就無(wú)法再通過(guò)exports來(lái)導(dǎo)出模塊內(nèi)容,除非通過(guò)exports=module.exports重新設(shè)置exports的引用指向。
當(dāng)然,可以先定義函數(shù),再導(dǎo)出:
function testModule() {
console.log('exports');
}
module.exports = testModule;
這是僅導(dǎo)出一個(gè)函數(shù)的情況,使用時(shí)就是這樣的:
testModule = require('./MyModule');
testModule();
如果是導(dǎo)出多個(gè)函數(shù),就可以這樣:
function testModule1() {
console.log('exports1');
}
function testModule2() {
console.log('exports2');
}
導(dǎo)入多個(gè)函數(shù)并使用:
({testModule1, testModule2} = require('./MyModule'));
testModule1();
testModule2();
② 模塊導(dǎo)入
可以通過(guò)以下方式來(lái)導(dǎo)入模塊:
const module = require('./MyModule');
注意,如果 require 的路徑?jīng)]有后綴,會(huì)自動(dòng)按照.js、.json和.node的順序進(jìn)行補(bǔ)齊查找。
③ 加載過(guò)程
在 CommonJS 中,require 的加載過(guò)程如下:
- 優(yōu)先從緩存中加載;
- 如果緩存中沒(méi)有,檢查是否是核心模塊,如果是直接加載;
- 如果不是核心模塊,檢查是否是文件模塊,解析路徑,根據(jù)解析出的路徑定位文件,然后執(zhí)行并加載;
- 如果以上都不是,沿當(dāng)前路徑向上逐級(jí)遞歸,直到根目錄的node_modules目錄。
(3)示例
下面來(lái)看一個(gè)購(gòu)物車(chē)的例子,主要功能是將商品添加到購(gòu)物車(chē),并計(jì)算購(gòu)物車(chē)商品總價(jià)格:
// cart.js
var items = [];
function addItem (name, price)
item.push({
name: name,
price: price
});
}
exports.total = function () {
return items.reduce(function (a, b) {
return a + b.price;
}, 0);
};
exports.addItem = addItem;
這里通過(guò)兩種方式在 exports 對(duì)象上定義了兩個(gè)方法:addItem 和 total,分別用來(lái)添加購(gòu)物車(chē)和計(jì)算總價(jià)。
下面在控制臺(tái)測(cè)試一下上面定義的模塊:
let cart = require('./cart');
這里使用相對(duì)路徑來(lái)導(dǎo)入 cart 模塊,打印 cart 模塊,結(jié)果如下:
cart // { total: [Function], addItem: [Function: addItem] }
向購(gòu)物車(chē)添加一些商品,并計(jì)算當(dāng)前購(gòu)物車(chē)商品的總價(jià)格:
cart.addItem('book', 60);
cart.total() // 60
cart.addItem('pen', 6);
cart.total() // 66
這就是創(chuàng)建模塊的基本方法,我們可以創(chuàng)建一些方法,并且只公開(kāi)希望其他文件使用的部分代碼。該部分成為 API,即應(yīng)用程序接口。
這里有一個(gè)問(wèn)題,只有一個(gè)購(gòu)物車(chē),即只有一個(gè)模塊實(shí)例。下面來(lái)在控制臺(tái)執(zhí)行以下代碼:
second_cart = require('./cart');
那這時(shí)會(huì)創(chuàng)建一個(gè)新的購(gòu)物車(chē)嗎?事實(shí)并非如此,打印當(dāng)前購(gòu)物車(chē)的商品總金額,它仍然是66:
second_cart.total(); // 66
當(dāng)我們?創(chuàng)建多個(gè)實(shí)例時(shí),就需要再模塊內(nèi)創(chuàng)建一個(gè)構(gòu)造函數(shù),下面來(lái)重寫(xiě) cart.js 文件:
// cart.js
function Cart () {
this.items = [];
}
Cart.prototype.addItem = function (name, price) {
this.items.push({
name: name,
price: price
});
}
Cart.prototype.total = function () {
return this.items.reduce(function(a, b) {
return a + b.price;
}, 0);
};
module.export = Cart;
現(xiàn)在,當(dāng)需要使用此模塊時(shí),返回的是 Cart 構(gòu)造函數(shù),而不是具有 cart 函數(shù)作為一個(gè)屬性的對(duì)象。下面來(lái)導(dǎo)入這個(gè)模塊,并創(chuàng)建兩個(gè)購(gòu)物車(chē)實(shí)例:
Cart = require('./second_cart');
cart1 = new Cart();
cart2 = new Cart();
cart1.addItem('book', 50);
cart1.total(); // 50
cart2.total(); // 50
4. AMD
(1)概念
CommonJS 的缺點(diǎn)之一是它是同步的,AMD 旨在通過(guò)規(guī)范中定義的 API 異步加載模塊及其依賴(lài)項(xiàng)來(lái)解決這個(gè)問(wèn)題。AMD 全稱(chēng)為 Asynchronous Module Definition,即異步模塊加載機(jī)制。它規(guī)定了如何定義模塊,如何對(duì)外輸出,如何引入依賴(lài)。
AMD規(guī)范重要特性就是異步加載。所謂異步加載,就是指同時(shí)并發(fā)加載所依賴(lài)的模塊,當(dāng)所有依賴(lài)模塊都加載完成之后,再執(zhí)行當(dāng)前模塊的回調(diào)函數(shù)。這種加載方式和瀏覽器環(huán)境的性能需求剛好吻合。
① 語(yǔ)法
AMD 規(guī)范定義了一個(gè)全局函數(shù) define,通過(guò)它就可以定義和引用模塊,它有 3 個(gè)參數(shù):
define(id?, dependencies?, factory);
其包含三個(gè)參數(shù):
- id:可選,指模塊路徑。如果沒(méi)有提供該參數(shù),模塊名稱(chēng)默認(rèn)為模塊加載器請(qǐng)求的指定腳本的路徑。
- dependencies:可選,指模塊數(shù)組。它定義了所依賴(lài)的模塊。依賴(lài)模塊必須根據(jù)模塊的工廠函數(shù)優(yōu)先級(jí)執(zhí)行,并且執(zhí)行的結(jié)果應(yīng)該按照依賴(lài)數(shù)組中的位置順序以參數(shù)的形式傳入工廠函數(shù)中。
- factory:為模塊初始化要執(zhí)行的函數(shù)或?qū)ο?。如果是函?shù),那么該函數(shù)是單例模式,只會(huì)被執(zhí)行一次;如果是對(duì)象,此對(duì)象應(yīng)該為模塊的輸出值。
除此之外,要想使用此模塊,就需要使用規(guī)范中定義的 require 函數(shù):
require(dependencies?, callback);
其包含兩個(gè)參數(shù):
- dependencies:依賴(lài)項(xiàng)數(shù)組;
- callback:加載模塊時(shí)執(zhí)行的回調(diào)函數(shù)。
有關(guān) AMD API 的更詳細(xì)說(shuō)明,可以查看 GitHub 上的 AMD API 規(guī)范:https://github.com/amdjs/amdjs-api/blob/master/AMD.md。
② 兼容性
該規(guī)范的瀏覽器兼容性如下:
③ 優(yōu)缺點(diǎn)
AMD 的優(yōu)點(diǎn):
- 異步加載導(dǎo)致更好的啟動(dòng)時(shí)間;
- 能夠?qū)⒛K拆分為多個(gè)文件;
- 支持構(gòu)造函數(shù);
- 無(wú)需額外工具即可在瀏覽器中工作。
AMD 的缺點(diǎn):
- 語(yǔ)法很復(fù)雜,學(xué)習(xí)成本高;
- 需要一個(gè)像 RequireJS 這樣的加載器庫(kù)來(lái)使用 AMD。
(2)使用
當(dāng)然,上面只是 AMD 規(guī)范的理論,要想理解這個(gè)理論在代碼中是如何工作的,就需要來(lái)看看 AMD 的實(shí)際實(shí)現(xiàn)。RequireJS 就是 AMD 規(guī)范的一種實(shí)現(xiàn),它被描述為“JavaScript 文件和模塊加載器”。下面就來(lái)看看 RequireJS 是如何使用的。
① 引入RequireJS
可以通過(guò) npm 來(lái)安裝 RequireJS:
npm i requirejs
也可以在 html 文件引入 require.js 文件:
<script data-main="js/config" src="js/require.js"></script>
這里 script標(biāo)簽有兩個(gè)屬性:
- data-main="js/config":這是 RequireJS 的入口,也是配置它的地方;
- src="js/require.js":加載腳本的正常方式,會(huì)加載 require.js 文件。
在 script 標(biāo)簽下添加以下代碼來(lái)初始化 RequireJS:
<script>
require(['config'], function() {
//...
})
</script>
當(dāng)頁(yè)面加載完配置文件之后, require() 中的代碼就會(huì)運(yùn)行。這個(gè) script 標(biāo)簽是一個(gè)異步調(diào)用,這意味著當(dāng) RequireJS 通過(guò) src="js/require.js 加載時(shí),它將異步加載 data-main 屬性中指定的配置文件。因此,該標(biāo)簽下的任何 JavaScript 代碼都可以在 RequireJS 獲取時(shí)執(zhí)行配置文件。
那 AMD 中的 require() 和 CommonJS 中的 require() 有什么區(qū)別呢?
- AMD require() 接受一個(gè)依賴(lài)數(shù)組和一個(gè)回調(diào)函數(shù),CommonJS require() 接受一個(gè)模塊 ID;
- AMD require() 是異步的,而 CommonJS require() 是同步的。
② 定義 AMD 模塊
下面是 AMD 中的一個(gè)基本模塊定義:
define(['dependency1', 'dependency2'], function() {
// 模塊內(nèi)容
});
這個(gè)模塊定義清楚地顯示了其包含兩個(gè)依賴(lài)項(xiàng)和一個(gè)函數(shù)。
下面來(lái)定義一個(gè)名為addition.js的文件,其包含一個(gè)執(zhí)行加法操作的函數(shù),但是沒(méi)有依賴(lài)項(xiàng):
// addition.js
define(function() {
return function(a, b) {
alert(a + b);
}
});
再來(lái)定義一個(gè)名為 calculator.js 的文件:
define(['addition'], function(addition) {
addition(7, 9);
});
當(dāng) RequireJS 看到上面的代碼塊時(shí),它會(huì)去尋找依賴(lài)項(xiàng),并通過(guò)將它們作為參數(shù)傳遞給函數(shù)來(lái)自動(dòng)將其注入到模塊中。
RequireJS 會(huì)自動(dòng)為 addition.js 和 calculator.js 文件創(chuàng)建一個(gè) <script> 標(biāo)簽,并將其放在HTML <head> 元素中,等待它們加載,然后運(yùn)行函數(shù),這類(lèi)似于 require() 的行為。
下面來(lái)更新一下 index.html 文件:
// index.html
require(['config'], function() {
require(['calculator']);
});
當(dāng)瀏覽器加載 index.html 文件時(shí),RequireJS 會(huì)嘗試查找 calculator.js 模塊,但是沒(méi)有找到,所以瀏覽器也不會(huì)有任何反應(yīng)。那該如何解決這個(gè)問(wèn)題呢?我們必須提供配置文件來(lái)告訴 RequireJS 在哪里可以找到 calculator.js(和其他模塊),因?yàn)樗且玫娜肟凇?/p>
下面是配置文件的基本結(jié)構(gòu):
requirejs.config({
baseURL: "string",
paths: {},
shim: {},
});
這里有三個(gè)屬性值:
- baseURL:告訴 RequireJS 在哪里可以找到模塊;
- path:這些是與 define() 一起使用的模塊的名稱(chēng)。 在路徑中,可以使用文件的 CDN,這時(shí) RequireJS 將嘗試在本地可用的模塊之前加載模塊的 CDN 版本;
- shim:允許加載未編寫(xiě)為 AMD 模塊的庫(kù),并允許以正確的順序加載它們
我們的配置文件如下:
requirejs.config({
baseURL: "js",
paths: {
// 這種情況下,模塊位于 customScripts 文件中
addition: "customScripts/addition",
calculator: "customScripts/calculator",
},
});
配置完成之后,重新加載瀏覽器,就會(huì)收到瀏覽器的彈窗:
這就是在 AMD 中使用 RequireJS 定義模塊的方法之一。我們還可以通過(guò)指定其路徑名來(lái)定義模塊,該路徑名是模塊文件在項(xiàng)目目錄中的位置。 下面給出一個(gè)例子:
define("path/to/module", function() {
// 模塊內(nèi)容
})
當(dāng)然,RequireJS 并不鼓勵(lì)這種方法,因?yàn)楫?dāng)我們將模塊移動(dòng)到項(xiàng)目中的另一個(gè)位置時(shí),就需要手動(dòng)更改模塊中的路徑名。
在使用 AMD 定義模塊時(shí)需要注意:
- 在依賴(lài)項(xiàng)數(shù)組中列出的任何內(nèi)容都必須與工廠函數(shù)中的分配相匹配;
- 盡量不要將異步代碼與同步代碼混用。當(dāng)在 index.html 上編寫(xiě)其他 JavaScript 代碼時(shí)就是這種情況。
5. CMD
CMD 全稱(chēng)為 Common Module Definition,即通用模塊定義。CMD 規(guī)范整合了 CommonJS 和 AMD 規(guī)范的特點(diǎn)。sea.js 是 CMD 規(guī)范的一個(gè)實(shí)現(xiàn) 。
CMD 定義模塊也是通過(guò)一個(gè)全局函數(shù) define 來(lái)實(shí)現(xiàn)的,但只有一個(gè)參數(shù),該參數(shù)既可以是函數(shù)也可以是對(duì)象:
define(factory);
如果這個(gè)參數(shù)是對(duì)象,那么模塊導(dǎo)出的就是對(duì)象;如果這個(gè)參數(shù)為函數(shù),那么這個(gè)函數(shù)會(huì)被傳入 3 個(gè)參數(shù):
define(function(require, exports, module) {
//...
});
這三個(gè)參數(shù)分別如下: (1)require:一個(gè)函數(shù),通過(guò)調(diào)用它可以引用其他模塊,也可以調(diào)用 require.async 函數(shù)來(lái)異步調(diào)用模塊; (2)exports:一個(gè)對(duì)象,當(dāng)定義模塊的時(shí)候,需要通過(guò)向參數(shù) exports 添加屬性來(lái)導(dǎo)出模塊 API; (3)module 是一個(gè)對(duì)象,它包含 3 個(gè)屬性:
- uri:模塊完整的 URI 路徑;
- dependencies:模塊依賴(lài);
- exports:模塊需要被導(dǎo)出的 API,作用同第二個(gè)參數(shù) exports。
下面來(lái)看一個(gè)例子,定義一個(gè) increment 模塊,引用 math 模塊的 add 函數(shù),經(jīng)過(guò)封裝后導(dǎo)出成 increment 函數(shù):
define(function(require, exports, module) {
var add = require('math').add;
exports.increment = function(val) {
return add(val, 1);
};
module.id = "increment";
});
CMD 最大的特點(diǎn)就是懶加載,不需要在定義模塊的時(shí)候聲明依賴(lài),可以在模塊執(zhí)行時(shí)動(dòng)態(tài)加載依賴(lài)。除此之外,CMD 同時(shí)支持同步加載模塊和異步加載模塊。
AMD 和 CMD 的兩個(gè)主要區(qū)別如下:
- AMD 需要異步加載模塊,而 CMD 在加載模塊時(shí),可以同步加載(require),也可以異步加載(require.async)。
- CMD 遵循依賴(lài)就近原則,AMD 遵循依賴(lài)前置原則。也就是說(shuō),在 AMD 中,需要把模塊所需要的依賴(lài)都提前在依賴(lài)數(shù)組中聲明。而在 CMD 中,只需要在具體代碼邏輯內(nèi),使用依賴(lài)前,把依賴(lài)的模塊 require 進(jìn)來(lái)。
6. UMD
UMD 全程為 Universal Module Definition,即統(tǒng)一模塊定義。其實(shí) UMD 并不是一個(gè)模塊管理規(guī)范,而是帶有前后端同構(gòu)思想的模塊封裝工具。
UMD 是一組同時(shí)支持 AMD 和 CommonJS 的模式,它旨在使代碼無(wú)論執(zhí)行代碼的環(huán)境如何都能正常工作,通過(guò) UMD 可以在合適的環(huán)境選擇對(duì)應(yīng)的模塊規(guī)范。比如在 Node.js 環(huán)境中采用 CommonJS 模塊管理,在瀏覽器環(huán)境且支持 AMD 的情況下采用 AMD 模塊,否則導(dǎo)出為全局函數(shù)。
一個(gè)UMD模塊由兩部分組成:
- **立即調(diào)用函數(shù)表達(dá)式 (IIFE)**:它會(huì)檢查使用模塊的環(huán)境。其有兩個(gè)參數(shù):root 和 factory。 root 是對(duì)全局范圍的 this 引用,而 factory 是定義模塊的函數(shù)。
- 匿名函數(shù): 創(chuàng)建模塊,此匿名函數(shù)被傳遞任意數(shù)量的參數(shù)以指定模塊的依賴(lài)關(guān)系。
UMD 的代碼實(shí)現(xiàn)如下:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports,
module.exports = factory();
} else {
root.returnExports = factory();
}
}(this, function () {
// 模塊內(nèi)容定義
return {};
}));
它的執(zhí)行過(guò)程如下:
- 先判斷是否支持 Node.js 模塊格式(exports 是否存在),存在則使用 Node.js 模塊格式;
- 再判斷是否支持 AMD(define 是否存在),存在則使用 AMD 方式加載模塊;
- 若兩個(gè)都不存在,則將模塊公開(kāi)到全局(Window 或 Global)。
UMD的特點(diǎn)如下:① UMD 的優(yōu)點(diǎn):
- 小而簡(jiǎn)潔;
- 適用于服務(wù)器端和客戶(hù)端。
② UMD 的缺點(diǎn):
- 不容易正確配置。
7. ES 模塊
(1)概念
通過(guò)上面的例子,你可能會(huì)發(fā)現(xiàn),使用 UMD、AMD、CMD 的代碼會(huì)變得難以編寫(xiě)和理解。于是在 2015 年,負(fù)責(zé) ECMAScript 規(guī)范的 TC39 委員會(huì)將模塊添加為 JavaScript 的內(nèi)置功能,這些模塊稱(chēng)為 ECMAScript模塊,簡(jiǎn)稱(chēng) ES 模塊。
模塊和經(jīng)典 JavaScript 腳本略有不同:
- 模塊默認(rèn)啟用嚴(yán)格模式,比如分配給未聲明的變量會(huì)報(bào)錯(cuò):
<script type="module">
a = 5;
</script>
- 模塊有一個(gè)詞法頂級(jí)作用域。 這意味著,例如,運(yùn)行 var foo = 42; 在模塊內(nèi)不會(huì)創(chuàng)建名為 foo 的全局變量,可通過(guò)瀏覽器中的 window.foo 訪問(wèn),盡管在經(jīng)典JavaScript腳本中會(huì)出現(xiàn)這種情況;
<script type="module">
let person = "Alok";
</script>
<script type="module">
alert(person);{/* Error: person is not defined */}
</script>
- 模塊中的 this 并不引用全局 this,而是 undefined。 (如果需要訪問(wèn)全局 this,可以使用 globalThis);
<script>
alert(this); {/* 全局對(duì)象 */}
</script>
<script type="module">
alert(this); {/* undefined */}
</script>
- 新的靜態(tài)導(dǎo)入和導(dǎo)出語(yǔ)法僅在模塊中可用,并不適用于經(jīng)典腳本。
- 頂層 await 在模塊中可用,但在經(jīng)典 JavaScript 腳本中不可用;
- await 不能在模塊中的任何地方用作變量名,經(jīng)典腳本中的變量可以在異步函數(shù)之外命名為 await;
- JavaScript 會(huì)提升 import 語(yǔ)句。因此,可以在模塊中的任何位置定義它們。
CommonJS 和 AMD 都是在運(yùn)行時(shí)確定依賴(lài)關(guān)系,即運(yùn)行時(shí)加載,CommonJS 加載的是拷貝。而 ES 模塊是在編譯時(shí)就確定依賴(lài)關(guān)系,所有加載的其實(shí)都是引用,這樣做的好處是可以執(zhí)行靜態(tài)分析和類(lèi)型檢查。
(2)語(yǔ)法
① 導(dǎo)出
當(dāng)導(dǎo)出模塊代碼時(shí),需要在其前面添加 export 關(guān)鍵詞。導(dǎo)出內(nèi)容可以是變量、函數(shù)或類(lèi)。任何未導(dǎo)出的代碼都是模塊私有的,無(wú)法在該模塊之被外訪問(wèn)。ES 模塊支持兩種類(lèi)型的導(dǎo)出:
- 命名導(dǎo)出:
export const first = 'JavaScript';
export function func() {
return true;
}
當(dāng)然,我們也可以先定義需要導(dǎo)出的變量/函數(shù),最后統(tǒng)一導(dǎo)出這些變量/函數(shù):
const first = 'JavaScript';
const second = 'TypeScript';
function func() {
return true;
}
export {first, second, func};
- 默認(rèn)導(dǎo)出:
function func() {
return true;
}
export default func;
當(dāng)然,也可以直接默認(rèn)導(dǎo)出:
export default function func() {
return true;
}
默認(rèn)導(dǎo)出可以省略變量/函數(shù)/類(lèi)名,在導(dǎo)入時(shí)可以為其指定任意名稱(chēng):
// 導(dǎo)出
export default function () {
console.log('foo');
}
// 導(dǎo)入
import customName from './module';
注意: 導(dǎo)入默認(rèn)模塊時(shí)不需要大括號(hào),導(dǎo)出默認(rèn)的變量或方法可以有名字,但是對(duì)外是無(wú)效的。export default 在一個(gè)模塊文件中只能使用一次。
可以使用 as 關(guān)鍵字來(lái)重命名需要暴露出的變量或方法,經(jīng)過(guò)重命名后同一變量可以多次暴露出去:
const first = 'test';
export {first as second};
② 導(dǎo)入
使用命名導(dǎo)出的模塊,可以通過(guò)以下方式來(lái)導(dǎo)入:
import {first, second, func} from './module';
使用默認(rèn)導(dǎo)出的模塊,可以通過(guò)以下方式來(lái)引入,導(dǎo)入名稱(chēng)可以自定義,無(wú)論導(dǎo)出的名稱(chēng)是什么:
import customName from './module.js';
導(dǎo)入模塊位置可以是相對(duì)路徑也可以是絕對(duì)路徑,.js擴(kuò)展名是可以省略的,如果不帶路徑而只是模塊名,則需要通過(guò)配置文件告訴引擎查找的位置:
import {firstName, lastName} from './module';
可以使用 as 關(guān)鍵字來(lái)將導(dǎo)入的變量/函數(shù)重命名:
import { fn as fn1 } from './profile';
在 ES 模塊中,默認(rèn)導(dǎo)入和命名導(dǎo)入是可以同時(shí)使用的,比如在 React 組件中:
import React, {usestate, useEffect} from 'react';
const Comp = () => {
return <React.Fragment>...</React.Fragment>
}
export default Comp;
可以使用 as 關(guān)鍵字來(lái)加載整個(gè)模塊,用于從另一個(gè)模塊中導(dǎo)入所有命名導(dǎo)出,會(huì)忽略默認(rèn)導(dǎo)出:
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長(zhǎng):' + circle.circumference(14));
③ 動(dòng)態(tài)導(dǎo)入
上面我們介紹的都是靜態(tài)導(dǎo)入,使用靜態(tài) import 時(shí),整個(gè)模塊需要先下載并執(zhí)行,然后主代碼才能執(zhí)行。有時(shí)我們不想預(yù)先加載模塊,而是按需加載,僅在需要時(shí)才加載。這可以提高初始加載時(shí)的性能,動(dòng)態(tài) import 使這成為可能:
<script type="module">
(async () => {
const moduleSpecifier = './lib.mjs';
const {repeat, shout} = await import(moduleSpecifier);
repeat('hello');
// → 'hello hello'
shout('Dynamic import in action');
// → 'DYNAMIC IMPORT IN ACTION!'
})();
</script>
與靜態(tài)導(dǎo)入不同,動(dòng)態(tài)導(dǎo)入可以在常規(guī)腳本中使用。
④ 其他用法
可以使用以下方式來(lái)先導(dǎo)入后導(dǎo)出模塊內(nèi)容:
export { foo, bar } from './module';
上面的代碼就等同于:
import { foo, bar } from './module';
export { foo, boo};
另一個(gè)與模塊相關(guān)的新功能是import.meta,它是一個(gè)給 JavaScript 模塊暴露特定上下文的元數(shù)據(jù)屬性的對(duì)象。它包含了這個(gè)模塊的信息,比如說(shuō)這個(gè)模塊的 URL。
默認(rèn)情況下,圖像是相對(duì)于 HTML 文檔中的當(dāng)前 URL 加載的。import.meta.url可以改為加載相對(duì)于當(dāng)前模塊的圖像:
function loadThumbnail(relativePath) {
const url = new URL(relativePath, import.meta.url);
const image = new Image();
image.src = url;
return image;
}
const thumbnail = loadThumbnail('../img/thumbnail.png');
container.append(thumbnail);
(3)在瀏覽器使用
目前主流瀏覽器都支持 ES 模塊:
如果想在瀏覽器中使用原生 ES 模塊方案,只需要在 script 標(biāo)簽上添加 type="module" 屬性。通過(guò)該屬性,瀏覽器知道這個(gè)文件是以模塊化的方式運(yùn)行的。而對(duì)于不支持的瀏覽器,需要通過(guò) nomodule 屬性來(lái)指定某腳本為 fallback 方案:
<script type="module">
import module1 from './module1'
</script>
<script nomodule src="fallback.js"></script>
支持 type="module" 的瀏覽器會(huì)忽略帶有 nomodule 屬性的腳本。使用 type="module" 的另一個(gè)作用就是進(jìn)行 ES Next 兼容性的嗅探。因?yàn)橹С?ES 模塊化的瀏覽器,都支持 ES Promise 等特性。
由于默認(rèn)情況下模塊是延遲的,因此可能還希望以延遲方式加載 nomodule 腳本:
<script nomodule defer src="fallback.js"></script>
(4)在 Node.js 使用
上面提到,Node.js 使用的是 CommonJS 模塊規(guī)范,它也是支持 ES 模塊的。在 Node.js 13 之前,ES 模塊是一項(xiàng)實(shí)驗(yàn)性技術(shù),因此,可以通過(guò)使用 .mjs 擴(kuò)展名保存模塊并通過(guò)標(biāo)志訪問(wèn)它來(lái)使用模塊。
從 Node.js 13 開(kāi)始,可以通過(guò)以下兩種方式使用模塊:
- 使用 .mjs 擴(kuò)展名保存模塊;
- 在最近的文件夾中創(chuàng)建一個(gè) type="module" 的 package.json 文件。
那如何在小于等于 12 版本的 Node.js 中使用 ES 模塊呢?可以在執(zhí)行腳本啟動(dòng)時(shí)加上 --experimental-modules,不過(guò)這一用法要求相應(yīng)的文件后綴名必須為 .mjs:
node --experimental-modules module1.mjs
import module1 from './module1.mjs'
module1