【webpack進(jìn)階】可視化展示webpack內(nèi)部插件與鉤子關(guān)系??

往期文章:

引言

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ù)雜性也可窺見一斑。

image

本文的第一部分會先介紹鉤子(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)系。

可視化工具使用效果圖:

image

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.jsshout.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.jsshout.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)一步整理):

image

但是梳理過程中其實出現(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)列出的compilercompilation中那些常用的鉤子,還存在著眾多其他可以使用的鉤子。有些有用的鉤子你可能無從知曉,例如我最近用到的localVarsrequireExtensions等鉤子。

  • 內(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)系展示:

image

可以通過點擊等交互,展示模塊內(nèi)鉤子信息,雙擊直接跳轉(zhuǎn)至webpack相應(yīng)源碼處:

image

由于關(guān)系非常復(fù)雜(600+關(guān)系),可以對關(guān)系類型進(jìn)行篩選,只展示關(guān)心的內(nèi)容:

image

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的小工具主要通過:

  1. 遍歷webpack源碼模塊文件
  2. 語法分析獲取鉤子相關(guān)信息
  3. 加工原始采集信息,轉(zhuǎn)換為力導(dǎo)向圖所需格式
  4. 基于力導(dǎo)向圖數(shù)據(jù)構(gòu)建前端web可視化服務(wù)
  5. 最后再輔以一些交互功能

目前我在使用它幫助閱讀與整理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)注!

往期文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內(nèi)容

  • 1 Webpack 1.1 概念簡介 1.1.1 WebPack是什么 1、一個打包工具 2、一個模塊加載工具 3...
    Kevin_Junbaozi閱讀 6,697評論 0 16
  • 過濾器 1. 理解過濾器 1)功能: 對要顯示的數(shù)據(jù)進(jìn)行特定格式化后再顯示 2)注意: 并沒有改變原本的數(shù)據(jù), 可...
    金政銳閱讀 250評論 0 0
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,618評論 25 708
  • 一:什么是閉包?閉包的用處? (1)閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。在本質(zhì)上,閉包就 是將函數(shù)內(nèi)部和函數(shù)外...
    xuguibin閱讀 9,690評論 1 52
  • 12月3號在喜馬拉雅上以優(yōu)惠價買了幾個音頻,今天一早上在聽孫宇晨的音頻。同為90后,但聽了他的音頻后,就感覺與他相...
    biliali閱讀 138評論 0 0