寫(xiě)在前面
這是webpack實(shí)戰(zhàn)系列的第二篇:模塊和模塊打包。上一篇:webpack實(shí)戰(zhàn)——打包第一個(gè)應(yīng)用 記錄了webpack的一些基礎(chǔ)內(nèi)容與一個(gè)簡(jiǎn)單地小例子,開(kāi)啟了webpack的實(shí)戰(zhàn)之路,這一篇記錄一下關(guān)于模塊和模塊打包。
模塊
先看一下模塊的定義:
模塊,是能夠單獨(dú)命名并獨(dú)立地完成一定功能的程序語(yǔ)句的集合(即程序代碼和數(shù)據(jù)結(jié)構(gòu)的集合體)。它具有兩個(gè)基本的特征:外部特征和內(nèi)部特征。外部特征是指模塊跟外部環(huán)境聯(lián)系的接口(即其他模塊或程序調(diào)用該模塊的方式,包括有輸入輸出參數(shù)、引用的全局變量)和模塊的功能;內(nèi)部特征是指模塊的內(nèi)部環(huán)境具有的特點(diǎn)(即該模塊的局部數(shù)據(jù)和程序代碼)。
可以從定義中看出,每個(gè)獨(dú)立的模塊負(fù)責(zé)不同工作,彼此之間又可以聯(lián)系在一起共同保證整體系統(tǒng)運(yùn)行。那么在webpack中,如何將其打包成一個(gè)(或多個(gè))文件呢?
想了解這些,我們還是先要熟悉在 Javascript 中的模塊。在 Javascript 模塊化中比較常見(jiàn)的有:
- CommonJS
- ES6 module
- AMD
- CMD
- UMD(AMD和CommonJS)
- ...
但由于在目前的使用場(chǎng)景中 CommonJS 和 ES6 module 居多,因此暫時(shí)就這兩者進(jìn)行討論。
1. CommonJS
1.1 模塊
在 CommonJS 中規(guī)定每個(gè)文件都是一個(gè)模塊。
在 CommonJS 中,變量及函數(shù)的聲明不會(huì)造成全局污染。如:
// add.js
var name = 'name: add.js';
// index.js
var name = 'name: index.js';
reuqire('./add.js');
console.log(name); // name: index.js
在上面 index.js 中通過(guò) require 函數(shù)來(lái)加載 add.js ,輸出的結(jié)果是 name: index.js ,說(shuō)明在 add 中定義的變量并不會(huì)影響 index ,可以得出使用 CommonJs 模塊,作用域只針對(duì)于該模塊,而不會(huì)造成全局污染,對(duì)外不可見(jiàn)。
1.2 導(dǎo)出
前面說(shuō)過(guò)模塊擁有自己的作用域,那么模塊是需要向外傳遞的,怎么辦呢?
導(dǎo)出是一個(gè)模塊向外暴露自身的唯一方式。在 CommonJS 中,我們通過(guò) module.exports 來(lái)導(dǎo)出模塊中的內(nèi)容。如:
// add.js
module.exports = {
name: 'add',
add: function(a, b) {
return a + b;
}
}
在 CommonJS 內(nèi)部會(huì)有一個(gè) module 對(duì)象用于存放當(dāng)前模塊的信息。而 module.exports 則指定向外暴露的內(nèi)容。
1.3 導(dǎo)入
導(dǎo)出自然是為了另外一個(gè)模塊來(lái)使用,這時(shí)便使用到了導(dǎo)入功能。在 CommonJS 中,使用 require 來(lái)進(jìn)行模塊導(dǎo)入:
// add.js
module.exports = {
name: 'add',
add: function(a, b) {
return a + b;
}
}
// index.js
const add = require('./add.js');
const sum = add.add(1, 2);
console.log(sum); // 3
上面這個(gè)例子,便是在 index.js 中通過(guò) require 導(dǎo)入了 add.js ,并且調(diào)用了其中的 add() 方法。
而我們?cè)?reuqire 一個(gè)模塊的時(shí)候,會(huì)分兩種情況:
- 如果 require 的模塊第一次被加載,那么會(huì)執(zhí)行該模塊然后導(dǎo)出內(nèi)容;
- 如果非首次加載,那么該模塊代碼不會(huì)再次執(zhí)行,而是直接導(dǎo)出上次代碼執(zhí)行后所得到的結(jié)果。
有時(shí)候我們只想通過(guò)加載執(zhí)行某個(gè)模塊讓它產(chǎn)生某種作用而不需要獲取它所導(dǎo)出的內(nèi)容,則可以直接通過(guò) require 來(lái)導(dǎo)入而不需要定義:
require('./task.js');
...
而通過(guò)這個(gè)特性,加上 require 函數(shù)可以接收表達(dá)式,那么我們則可以動(dòng)態(tài)指定模塊加載路徑:
const moduleList = ['add.js', 'subtract.js'];
moduleList.forEach(item => {
require(`./${item}`);
});
2. ES6 Module
熟悉 JavaScript 語(yǔ)言的小伙伴知道,其實(shí)在 JavaScript 設(shè)計(jì)之初,并沒(méi)有模塊化這個(gè)概念。而伴隨 JavaScript 不斷的壯大發(fā)展,社區(qū)中也涌現(xiàn)出了不少模塊化概念。一直到 ES6 , JavaScript 終于正式的有了模塊化這一特性。
2.1 模塊
在前面我們使用 CommonJS 實(shí)現(xiàn)了一個(gè)例子來(lái)展示 CommonJS 的模塊、導(dǎo)出與導(dǎo)入,同樣在此處也先來(lái)一個(gè)例子,只需將上面例子稍微改寫(xiě)即可:
// add.js
export default {
name: 'add',
add: function(a, b) {
return a + b;
}
}
// index.js
import add from './add.js';
const sum = add.add(-1, 1);
console.log(sum); // 0
ES6 Module 也是將每一個(gè)文件都作為一個(gè)模塊,并且每個(gè)模塊擁有自身的作用域,但是與 CommonJS 相比, 不同的是導(dǎo)入、導(dǎo)出語(yǔ)句。在 ES6 Module 中,
import 和 export 也作為關(guān)鍵字被保留。
2.2 導(dǎo)出
在 ES6 Module 中,使用 export 來(lái)對(duì)模塊進(jìn)行導(dǎo)出。
export 導(dǎo)出的兩種方式:
- 命名導(dǎo)出
- 默認(rèn)導(dǎo)出
2.2.1 命名導(dǎo)出
以下有兩種寫(xiě)法,但其效果并無(wú)區(qū)別:
/**
* 命名導(dǎo)出: 兩種寫(xiě)法
**/
// 1. 聲明和導(dǎo)出寫(xiě)在一起
export const name = 'add';
export const add = function(a, b) {
return a + b;
}
// 2. 先聲明,再統(tǒng)一導(dǎo)出
const name = 'add';
const add = function(a, b) {
return a + b;
}
export { name, add }
as關(guān)鍵字
在使用命名導(dǎo)出時(shí),如果用寫(xiě)法2(先聲明再統(tǒng)一導(dǎo)出),可以使用 as 關(guān)鍵字 來(lái)對(duì)導(dǎo)出的變量進(jìn)行重命名。如:
const name = 'add';
const add = function(a, b) {
return a + b;
}
// add as sum : 在導(dǎo)入時(shí),使用 name 和 sum 即可
export { name, add as sum }
2.2.2 默認(rèn)導(dǎo)出
說(shuō)完了命名導(dǎo)出,來(lái)到默認(rèn)導(dǎo)出:模塊的默認(rèn)導(dǎo)出只能導(dǎo)出一個(gè)。舉例:
// 默認(rèn)導(dǎo)出
export defailt {
name: 'add',
add: function(a, b) {
return a + b;
}
}
由上可見(jiàn),我們可以將默認(rèn)導(dǎo)出理解為向外輸出了一個(gè)命名為 default 的變量。
2.3 導(dǎo)入
ES6 Module 中使用 import 進(jìn)行模塊導(dǎo)入。由于在 ES6 Module 的導(dǎo)出中,分為 命名導(dǎo)出 和 默認(rèn)導(dǎo)出 ,因此在導(dǎo)入的時(shí)候也有對(duì)應(yīng)的兩種方式進(jìn)行導(dǎo)入。
2.3.1 命名
// add.js
const name = 'add';
const add = function(a, b) {
return a + b;
}
export { name, add }
// index.js
import { name, add } from './add.js';
console.log(name, add(1, 2)); // add 3
可以看到,在使用 import 對(duì)命名導(dǎo)出模塊進(jìn)行引入的時(shí)候, import 后面跟了一對(duì)花括號(hào) { } 將導(dǎo)入的變量名包裹起來(lái),并且變量名需要與導(dǎo)出時(shí)的變量命名一樣。同樣,我們還是可以使用 as 關(guān)鍵字 來(lái)對(duì)變量進(jìn)行重命名:
// index.js
import { name, add as sum } from './add.js'
sum(2, 2); // 4
值得注意的是,導(dǎo)入變量的效果相當(dāng)于在當(dāng)前作用域下聲明了變量(如 name 和 add),但不可對(duì)這些變量不能修改,只可當(dāng)成只讀的來(lái)使用。
當(dāng)然,我們還可以使用 * 來(lái)進(jìn)行整體導(dǎo)入:
// index.js
import * as add from './add.js';
console.log(add.name); // add
console.log(add.add(1, 1)); // 2
2.3.2 默認(rèn)
對(duì)于默認(rèn)導(dǎo)出的導(dǎo)入處理如下:
// add.js
export default {
name: 'add',
add: function(a, b) {
return a + b;
}
}
// index.js
import addMe from './add.js';
console.log(addMe.name, addMe.add(2, 5)); // add 7
可以看到,如果是導(dǎo)入默認(rèn)導(dǎo)出的模塊,那么在 import 后面直接跟變量名即可,并且這個(gè)變量名無(wú)需與導(dǎo)出模塊的變量名重復(fù),可以自己指定新的變量名。
3. CommonJS 與 ES6 Module 的區(qū)別
介紹了 CommonJS 與 ES6 Module 的基礎(chǔ)應(yīng)用之后,我們也要了解到在實(shí)際的開(kāi)發(fā)過(guò)程中我們經(jīng)常將這兩者在同一個(gè)項(xiàng)目中混用。為了避免不必要的麻煩,還是要說(shuō)一下兩者的異同。
3.1 動(dòng)態(tài)與靜態(tài)
CommonJS 對(duì)模塊依賴(lài)的解決是動(dòng)態(tài)的,而 ES6 Module 對(duì)模塊依賴(lài)的解決是靜態(tài)的。
首先要了解這里說(shuō)的動(dòng)態(tài)與靜態(tài)是什么:
- 動(dòng)態(tài): 模塊依賴(lài)關(guān)系的建立發(fā)生在代碼
運(yùn)行階段; - 靜態(tài): 模塊依賴(lài)關(guān)系的建立發(fā)生在代碼
編譯階段;
由于 ES6 Module 中導(dǎo)入導(dǎo)出語(yǔ)句都是聲明式的,不支持導(dǎo)入表達(dá)式類(lèi)路徑,并且導(dǎo)入導(dǎo)出語(yǔ)句必須位于模塊的頂層作用域。相比 CommonJS ,具備優(yōu)勢(shì)如下:
- 死代碼檢測(cè)和排除: 可以使用靜態(tài)分析工具檢測(cè)出沒(méi)有被調(diào)用的模塊,減小打包資源體積;
- 模塊變量類(lèi)型檢查: 有助于確保模塊之間傳遞的值或者接口類(lèi)型的正確性;
- 編譯器優(yōu)化: ES6 Module 直接導(dǎo)入變量,減少層級(jí)引用,程序效率更高。
3.2 值拷貝和動(dòng)態(tài)映射
在導(dǎo)入一個(gè)模塊時(shí),對(duì)于 CommonJS 來(lái)說(shuō)獲取的是一份導(dǎo)出值的拷貝,而在 ES6 Module 中則是值的動(dòng)態(tài)映射,這個(gè)映射是只讀的。例如:
// add.js
var count = 0;
module.exports = {
count: count,
add: function(a, b) {
count += 1;
return a + b;
}
}
// index.js
var count = require('./add.js').count;
var add = require('./add/js').add;
console.log(count); // 0 (這里的count是對(duì)add.js中couunt的拷貝)
add(2, 3);
console.log(count); // 0 (add.js中變量值的改變不會(huì)對(duì)這里的拷貝值造成影響)
count += 1;
console.log(count); // 1 (拷貝的值 0 + 1 = 1,表示拷貝的值可以更改)
可以看出,index.js 中的 count 是 add.js 中 count 的一份值拷貝,因此在調(diào)用 add 函數(shù)時(shí),即便更改了 add 中的 count,但不會(huì)對(duì) index.js 中的值拷貝造成影響。
但在 ES6 Module 中,卻不一樣:
// add.js
let count = 0;
const add = function(a, b) {
count += 1;
return a + b;
};
export { count, add };
// index.js
import { count, add } from './add.js';
console.log(count); // 0 (對(duì)add.js中count值的映射)
add(1, 1);
console.log(count); // 1 (對(duì)add.js中count值的映射,會(huì)映射值的變化)
count += 1; // 報(bào)錯(cuò),該count值不可更改
4. 模塊打包原理
前面描述了一些基礎(chǔ)的 CommonJS 與 ES6 Module 模塊化的一些知識(shí),那么回到 webpack 中來(lái):webpack是如何將各種模塊有序的組織在一起的呢?
// add.js
module.exports = {
add: function(a, b) {
return a + b;
}
}
// index.js
const add = require('./add.js');
const sum = add(1, 2);
console.log(`sum: ${sum}`);
還是之前的例子,但現(xiàn)在經(jīng)過(guò) webpack 打包之后,它會(huì)變成什么樣子呢?

如圖所示,這便是一個(gè)簡(jiǎn)單地打包結(jié)果。我們可以觀(guān)察自己的 bundle.js 文件,從中看打包邏輯關(guān)系:
- 首先一個(gè)立即執(zhí)行匿名函數(shù),包裹所有內(nèi)容,構(gòu)成資深作用域;
- installedModule對(duì)象(模塊緩存),每個(gè)模塊在第一次被加載的時(shí)候執(zhí)行,到處結(jié)果存儲(chǔ)到其中,以后再次調(diào)用模塊直接取值即可,不會(huì)再次執(zhí)行模塊;
- webpack_require函數(shù): 對(duì)模塊加載的實(shí)現(xiàn),在瀏覽器中可以通過(guò)調(diào)用此函數(shù)加模塊id來(lái)進(jìn)行模塊導(dǎo)入;
- modules對(duì)象:工程中所有產(chǎn)生依賴(lài)關(guān)系的模塊都會(huì)以
key-value形式放在此對(duì)象中, key 作為模塊 id,由數(shù)字或者 hash 字符串構(gòu)成,value 則由一個(gè)匿名函數(shù)包裹的模塊構(gòu)成,匿名函數(shù)的參數(shù)則賦予了每個(gè)模塊導(dǎo)出和導(dǎo)入能力。
小結(jié)
本篇記錄了關(guān)于 JavaScript 的模塊化與 webpack 的模塊打包原理簡(jiǎn)介。
首先,介紹了關(guān)于模塊的概念,然后依次介紹了兩種模塊化:CommonJS 和 ES6 Module ,以及他們分別的模塊概念、導(dǎo)出和導(dǎo)入,接著介紹了他們之間的兩個(gè)差異:動(dòng)態(tài)與靜態(tài)、值拷貝和映射。最后,提及了一下模塊化打包的簡(jiǎn)單原理,對(duì)webpack打包工作有一個(gè)大概認(rèn)知。
下一篇將會(huì)介紹在webpack中資源的輸入與輸出。敬請(qǐng)期待。
學(xué)習(xí)推薦: 本系列學(xué)習(xí)資源多來(lái)自于 《webpack實(shí)戰(zhàn) 入門(mén)、進(jìn)階與調(diào)優(yōu)》 ,作者 居玉皓 , 感興趣的朋友可以購(gòu)買(mǎi)實(shí)體書(shū)支持學(xué)習(xí)~