往期文章:
引言
webpack的成功之處,不僅在于強大的打包構(gòu)建能力,也在于它靈活的插件機制。
也許你了解過webpack的插件與鉤子機制;但你或許不知道,webpack內(nèi)部擁有超過180個鉤子,這些鉤子與模塊(內(nèi)置插件)之間的「創(chuàng)建」「注冊」「調(diào)用」關(guān)系非常復(fù)雜。因此,掌握webpack內(nèi)部插件與鉤子間的關(guān)系會幫助我們更進(jìn)一步理解webpack的內(nèi)部執(zhí)行方式。
「webpack模塊/內(nèi)置插件與鉤子關(guān)系圖??」:復(fù)雜性也可窺見一斑。
本文的第一部分會先介紹鉤子(hook)這個重要的概念與webpack插件的工作方式。然而,熟悉的朋友會發(fā)現(xiàn),這種靈活的機制使得webpack模塊之間的聯(lián)系更加松散與非耦合的同時,讓想要理清webpack內(nèi)部源碼結(jié)構(gòu)與聯(lián)系變得更困難。
所以,第二部分將會介紹webpack內(nèi)部插件與鉤子關(guān)系的可視化展示工具??,用一張圖理清webpack內(nèi)部這種錯綜復(fù)雜的關(guān)系。
可視化工具使用效果圖:
1. webpack的插件機制
在具體介紹webpack內(nèi)置插件與鉤子可視化工具之前,我們先來了解一下webpack中的插件機制。
webpack實現(xiàn)插件機制的大體方式是:
- 「創(chuàng)建」—— webpack在其內(nèi)部對象上創(chuàng)建各種鉤子;
- 「注冊」—— 插件將自己的方法注冊到對應(yīng)鉤子上,交給webpack;
- 「調(diào)用」—— webpack編譯過程中,會適時地觸發(fā)相應(yīng)鉤子,因此也就觸發(fā)了插件的方法。
1.1. Tapable
Tapable就是webpack用來創(chuàng)建鉤子的庫。
The tapable packages exposes many Hook classes, which can be used to create hooks for plugins.
通過Tapable,可以快速創(chuàng)建各類鉤子。以下是各種鉤子的類函數(shù):
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
以最簡單的SyncHook
為例,它可以幫助我們創(chuàng)建一個同步的鉤子。為了幫助理解Tapable創(chuàng)建鉤子的使用方式,我們以一個“下班回家進(jìn)門”的模擬場景來介紹Tapable是如何使用的。
現(xiàn)在我們有一個welcome.js
模塊,它設(shè)定了我們“進(jìn)門回家”的一系列行為(開門、脫鞋…):
// welcome.js
const {SyncHook} = require('tapable');
module.exports = class Welcome {
constructor(words) {
this.words = words;
this.sayHook = new SyncHook(['words']);
}
// 進(jìn)門回家的一系列行為
begin() {
console.log('開門');
console.log('脫鞋');
console.log('脫外套');
// 打招呼
this.sayHook.call(this.words);
console.log('關(guān)門');
}
}
首先,我們在構(gòu)造函數(shù)里創(chuàng)建了一個同步鉤子sayHook
,它用來進(jìn)行之后的打招呼。
然后,begin()
方法描述了我們剛回家進(jìn)門的一系列動作:開門、脫鞋、脫外套、關(guān)門。其中,在「脫外套」與「關(guān)門」之間是一個打招呼的行為,我們在此觸發(fā)了sayHook
鉤子,并將words作為參數(shù)傳入其中。
注意,這里的
.call()
的方法是Tapable提供的觸發(fā)鉤子的方法,不是js中原生的call方法。
觸發(fā)這一系列流程也非常簡單:
// run.js
const Welcome = require('./welcome');
const welcome = new Welcome('我回來啦!');
welcome.begin();
/* output:
* 開門
* 脫鞋
* 脫外套
* 關(guān)門
* /
接下來,我們希望有不同的打招呼方式 —— “普通地打招呼”和“大喊一聲”。
對應(yīng)的我們會有兩個模塊say.js
和shout.js
,通過.tap()
方法在sayHook
鉤子上注冊相應(yīng)方法。
// say.js
module.exports = function (welcome) {
welcome.sayHook.tap('say', words => {
console.log('輕聲說:', words);
});
};
// shout.js
module.exports = function (welcome) {
welcome.sayHook.tap('shout', words => {
console.log('出其不意的大喊一聲:', words);
});
};
最后,我們修改一下run.js
,給welcome
應(yīng)用shout.js
這個模塊。
// run.js
const Welcome = require('./welcome');
const applyShoutPlugin = require('./shout');
const welcome = new Welcome('我回來啦!');
applyShoutPlugin(welcome);
welcome.begin();
/* output:
* 開門
* 脫鞋
* 脫外套
* 出其不意的大喊一聲: 我回來啦!
* 關(guān)門
* /
這樣,我們就把打招呼的實現(xiàn)方式與welcome解耦了。我們也可以使用say.js
模塊,甚至和shout.js
兩者同時使用。這就好比創(chuàng)建了一個“可插拔”的系統(tǒng)機制 —— 我可以根據(jù)需求自主選擇要不要打招呼,要用什么方式打招呼。
雖然上面的例子非常簡單,但是已經(jīng)可以幫助我們理解tapable的使用以及插件的思想。
1.2. webpack中的插件
在介紹webpack的插件機制前,先簡單回顧下上面“進(jìn)門回家”例子:
- 我們的
Welcome
類是主要的功能類,其中包含具體的功能函數(shù)begin()
與鉤子sayHook
; -
run.js
模塊負(fù)責(zé)執(zhí)行流程,控制代碼流; - 最后,
say.js
和shout.js
是獨立的“可插入”模塊。根據(jù)需要,我們可以自主附加到主流程中。
理解了上面這個例子,就可以很好地類比到webpack中:
例如,webpack中有一個重要的類 —— Compiler
,它創(chuàng)建了非常多的鉤子,這些鉤子將會散落在“各地”被調(diào)用(call)。它就類似于我們的Welcome
類。
// Compiler類中的部分鉤子
this.hooks = {
/** @type {SyncBailHook<Compilation>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<Stats>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {AsyncSeriesHook<>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<Compiler>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compiler>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compilation>} */
emit: new AsyncSeriesHook(["compilation"]),
……
}
然后,webpack中的插件會將所需執(zhí)行的函數(shù)通過 .tap()
/ .tapAsync()
/ .tapPromise()
等方法注冊到對應(yīng)鉤子上。這樣,webpack調(diào)用相應(yīng)鉤子時,插件中的函數(shù)就會自動執(zhí)行。
那么,還有一個問題:webpack是如何調(diào)用插件,將插件中的方法在編譯階段注冊到鉤子上的呢?
對于這個問題,webpack規(guī)定每個插件的實例,必須有一個.apply()
方法,webpack打包前會調(diào)用所有插件的.apply()
方法,插件可以在該方法中進(jìn)行鉤子的注冊。
在webpack的lib/webpack.js
中,有如下代碼:
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
}
上面這段代碼會從webpack配置的plugins
字段中取出所有插件的實例,然后調(diào)用其.apply()
方法,并將Compiler
的實例作為參數(shù)傳入。這就是為什么webpack要求我們所有插件都需要提供.apply()
方法,并在其中進(jìn)行鉤子的注冊。
注意,和
.call()
一樣,這里的.apply()
也不是js的原生方法。你會在源碼中看到許多.call()
與.apply()
,但它們基本都不是你認(rèn)識的那個方法。
2. 編譯期(Compiler中)鉤子的觸發(fā)流程
目前,網(wǎng)上已經(jīng)有了一些解析webpack的優(yōu)質(zhì)文章。其中也不乏對webpack編譯流程整理與介紹的文章。
但是,由于我近期的工作與興趣原因,需要對webpack內(nèi)部的執(zhí)行步驟與細(xì)節(jié)做一些較為深入的調(diào)研,包括各種鉤子與方法的注冊、觸發(fā)時機、條件等等。目前的一些文章內(nèi)容可能不足以支持,據(jù)此做了一定的整理工作。
2.1. 一張待完善的圖
下面是我之前梳理的Compiler
中.run()
方法(編譯的啟動方法)的執(zhí)行流程及鉤子觸發(fā)情況(圖中只涉及了一部分compilation的相關(guān)鉤子,完整版還需進(jìn)一步整理):
但是梳理過程中其實出現(xiàn)了一些困難。如果你也曾經(jīng)想要仔細(xì)閱讀webpack源碼并梳理內(nèi)部各個模塊與插件執(zhí)行流程與關(guān)系,可能也會碰到和我一樣的麻煩。下面就來說一下:
2.2. 插件與鉤子機制帶來的問題
首先,可以看到由于圖比較細(xì),所以它會比網(wǎng)上常見的整體流程圖要復(fù)雜;但是,即使只算上webpack常用插件、compiler
鉤子與compilation
鉤子,這張圖也只算是其中一小部分。更不用說另外上百個你可能從未接觸過的鉤子。這些模塊與鉤子交織出了一個復(fù)雜的webpack系統(tǒng)。
其次,在源碼閱讀與整理的過程中,還會遇到幾個問題:
聯(lián)系松散。根據(jù)以上的例子,你可以發(fā)現(xiàn):使用tapable鉤子類似事件監(jiān)聽模式,雖然能有效解耦,但鉤子的注冊與調(diào)用幾乎完全無關(guān),很難將一個鉤子的“創(chuàng)建 - 注冊 - 調(diào)用”過程有效聯(lián)系起來。
模塊交互基于鉤子。webpack內(nèi)部模塊與插件在很多時候,是通過鉤子機制來進(jìn)行聯(lián)系與調(diào)用的。但是,基于鉤子的模式是松散的。例如你看到源碼里一個模塊提供了幾個鉤子,但你并不知道,在何時、何地該鉤子會被調(diào)用,又在何時、何地鉤子上被注冊了哪些方法。這些以往都是需要我們通過在代碼庫中搜索關(guān)鍵詞來解決。
鉤子數(shù)量眾多。webpack內(nèi)部的鉤子非常多,數(shù)量達(dá)到了180+,類型也五花八門。除了官網(wǎng)列出的
compiler
與compilation
中那些常用的鉤子,還存在著眾多其他可以使用的鉤子。有些有用的鉤子你可能無從知曉,例如我最近用到的localVars
、requireExtensions
等鉤子。內(nèi)置插件眾多。webpack v4+ 本身內(nèi)置了許多插件。即使非插件,webpack的模塊自身也經(jīng)常使用tapable鉤子來交互。甚至可以認(rèn)為,webpack項目中的各個模塊都是“插件化”的。這也使得幾乎每個模塊都會和各種鉤子“打交道”。
這些問題導(dǎo)致了想要全面了解webpack中模塊/插件間作用關(guān)系(核心是與鉤子的關(guān)系)具有一定的困難。為了幫助理解與閱讀webpack源碼、理清關(guān)系,我制作了一個小工具來可視化展示內(nèi)置插件與鉤子之間的關(guān)系,并支持通過交互操作進(jìn)一步獲取源碼信息。
3. Webpack Internal Plugin Relation
Webpack-Internal-Plugin-Relation是一個可以展現(xiàn)webpack內(nèi)部模塊(插件)與鉤子間關(guān)系的工具。文章開頭展示的動圖就是其功能與使用效果。
github倉庫地址:https://github.com/alienzhou/webpack-internal-plugin-relation
可以在這里查看 在線演示
3.1. 關(guān)系類型
模塊/插件與鉤子的關(guān)系主要分為三類:
- 模塊/插件「創(chuàng)建」鉤子,如
this.hooks.say = new SyncHook()
; - 模塊/插件將方法「注冊」到鉤子上,如
obj.hooks.say.tap('one', () => {...})
; - 模塊/插件通過「調(diào)用」來觸發(fā)鉤子事件,如
obj.hooks.say.call()
。
3.2. 效果演示
可以進(jìn)行模塊/插件與鉤子之間的關(guān)系展示:
可以通過點擊等交互,展示模塊內(nèi)鉤子信息,雙擊直接跳轉(zhuǎn)至webpack相應(yīng)源碼處:
由于關(guān)系非常復(fù)雜(600+關(guān)系),可以對關(guān)系類型進(jìn)行篩選,只展示關(guān)心的內(nèi)容:
3.3. 工具包含的功能
具體來說,這個工具包含的功能主要包括:
-
關(guān)系收集:
- 收集模塊中hook的創(chuàng)建信息,即鉤子的創(chuàng)建信息;
- 收集模塊中hook的注冊信息,記錄哪些模塊對哪些鉤子進(jìn)行了注冊;
- 收集模塊中hook的調(diào)用信息,即鉤子是在代碼中的哪一行觸發(fā)的;
- 生成包含「模塊信息」、「鉤子信息」、「源碼位置信息」等原始數(shù)據(jù)的文件。
-
可視化展示:
- 使用力導(dǎo)向圖可視化展示插件、鉤子間關(guān)系。可以看到目前webpack v4中有超過180個鉤子與超過130個模塊;
- 展示所有模塊與鉤子列表。
-
交互信息:
- 支持對力導(dǎo)向圖中節(jié)點的展現(xiàn)進(jìn)行篩選;
- 通過單擊javascript module類節(jié)點,可在左下角查看模塊的詳細(xì)信息;
- 雙擊javascript module類節(jié)點,可直接打開webpack對應(yīng)源碼查看;
- 雙擊節(jié)點間關(guān)系,可直接打開并定位源碼具體行數(shù),進(jìn)行查看;
- 可以選擇要查看的關(guān)系:創(chuàng)建-contain / 注冊-register / 調(diào)用-call。
3.4. 基于原始數(shù)據(jù)定制自己的功能
目前,工具會將原始的采集結(jié)果都保留下來。因此,如果你并不需要可視化展示,或者有自己的定制化需求,那么完全可以基于這些信息進(jìn)行處理,用于你所需的地方。模塊的原始信息結(jié)構(gòu)如下:
"lib/MultiCompiler.js": {
"hooks": [
{
"name": "done",
"line": 17
},
{
"name": "invalid",
"line": 18
},
{
"name": "run",
"line": 19
},
{
"name": "watchClose",
"line": 20
},
{
"name": "watchRun",
"line": 21
}
],
"taps": [
{
"hook": "done",
"type": "tap",
"plugin": "MultiCompiler",
"line": 37
},
{
"hook": "invalid",
"type": "tap",
"plugin": "MultiCompiler",
"line": 48
}
],
"calls": [
{
"hook": "done",
"type": "call",
"line": 44
}
]
}
4. 尾聲
這個Webpack-Internal-Plugin-Relation的小工具主要通過:
- 遍歷webpack源碼模塊文件
- 語法分析獲取鉤子相關(guān)信息
- 加工原始采集信息,轉(zhuǎn)換為力導(dǎo)向圖所需格式
- 基于力導(dǎo)向圖數(shù)據(jù)構(gòu)建前端web可視化服務(wù)
- 最后再輔以一些交互功能
目前我在使用它幫助閱讀與整理webapck源碼與編譯流程。也許有些朋友也碰到了類似問題,分享出來希望它也能在某些方面對你有所幫助。如果你也對webpack或者這個工具感興趣,希望能多多支持我的文章和工具,一同交流學(xué)習(xí)~??
告別「webpack配置工程師」
寫在最后。
webpack是一個強大而復(fù)雜的前端自動化工具。其中一個特點就是配置復(fù)雜,這也使得「webpack配置工程師」這種戲謔的稱呼開始流行??但是,難道你真的只滿足于玩轉(zhuǎn)webpack配置么?
顯然不是。在學(xué)習(xí)如何使用webpack之外,我們更需要深入webpack內(nèi)部,探索各部分的設(shè)計與實現(xiàn)。萬變不離其宗,即使有一天webpack“過氣”了,但它的某些設(shè)計與實現(xiàn)卻仍會有學(xué)習(xí)價值與借鑒意義。因此,在學(xué)習(xí)webpack過程中,我會總結(jié)一系列【webpack進(jìn)階】的文章和大家分享。
歡迎感興趣的同學(xué)多多交流與關(guān)注!
往期文章: