webpack實(shí)戰(zhàn)——模塊打包

寫(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)景中 CommonJSES6 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ì)分兩種情況:

  1. 如果 require 的模塊第一次被加載,那么會(huì)執(zhí)行該模塊然后導(dǎo)出內(nèi)容;
  2. 如果非首次加載,那么該模塊代碼不會(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ì)變成什么樣子呢?

bundle.js

如圖所示,這便是一個(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í)~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容