談?wù)凾apable的前世今生
tapable 是一個(gè)類(lèi)似于 Node.js 中的 EventEmitter 的庫(kù),但更專(zhuān)注于自定義事件的觸發(fā)和處理。webpack 通過(guò) tapable 將實(shí)現(xiàn)與流程解耦,所有具體實(shí)現(xiàn)通過(guò)插件的形式存在。
Tapable 和 webpack 的關(guān)系
webpack 是什么?
本質(zhì)上,webpack 是一個(gè)用于現(xiàn)代 JavaScript 應(yīng)用程序的 靜態(tài)模塊打包工具。當(dāng) webpack 處理應(yīng)用程序時(shí),它會(huì)在內(nèi)部構(gòu)建一個(gè) 依賴(lài)圖(dependency graph),此依賴(lài)圖對(duì)應(yīng)映射到項(xiàng)目所需的每個(gè)模塊,并生成一個(gè)或多個(gè) bundle。
webpack 的重要模塊
- 入口(entry)
 - 輸出(output)
 - loader(對(duì)模塊的源代碼進(jìn)行轉(zhuǎn)換)
 - plugin(webpack 構(gòu)建流程中的特定時(shí)機(jī)注入擴(kuò)展邏輯來(lái)改變構(gòu)建結(jié)果或做你想要的事)
 
插件(plugin)是 webpack 的支柱功能。webpack 自身也是構(gòu)建于你在 webpack 配置中用到的相同的插件系統(tǒng)之上。
webpack 的構(gòu)建流程
webpack 本質(zhì)上是一種事件流的機(jī)制,它的工作流程就是將各個(gè)插件串聯(lián)起來(lái),而實(shí)現(xiàn)這一切的核心就是 Tapable。webpack 中最核心的負(fù)責(zé)編譯的 Compiler 和負(fù)責(zé)創(chuàng)建 bundle 的 Compilation 都是 Tapable 的實(shí)例(webpack5 前)。webpack5 之后是通過(guò)定義屬性名為 hooks 來(lái)調(diào)度觸發(fā)時(shí)機(jī)。Tapable 充當(dāng)?shù)木褪且粋€(gè)復(fù)雜的發(fā)布訂閱者模式
以 Compiler 為例:
- // webpack5 前,通過(guò)繼承
 - ...
 - const {
 - Tapable,
 - SyncHook,
 - SyncBailHook,
 - AsyncParallelHook,
 - AsyncSeriesHook
 - } = require("tapable");
 - ...
 - class Compiler extends Tapable {
 - constructor(context) {
 - super();
 - ...
 - }
 - }
 - // webpack5
 - ...
 - const {
 - SyncHook,
 - SyncBailHook,
 - AsyncParallelHook,
 - AsyncSeriesHook
 - } = require("tapable");
 - ...
 - class Compiler {
 - constructor(context) {
 - this.hooks = Object.freeze({
 - /** @type {SyncHook<[]>} */
 - initialize: new SyncHook([]),
 - /** @type {SyncBailHook<[Compilation], boolean>} */
 - shouldEmit: new SyncBailHook(["compilation"]),
 - ...
 - })
 - }
 - ...
 - }
 
Tapable 的使用姿勢(shì)
tapable 對(duì)外暴露了 9 種 Hooks 類(lèi)。這些 Hooks 類(lèi)的作用就是通過(guò)實(shí)例化來(lái)創(chuàng)建一個(gè)執(zhí)行流程,并提供注冊(cè)和執(zhí)行方法,Hook 類(lèi)的不同會(huì)導(dǎo)致執(zhí)行流程的不同。
- const {
 - SyncHook,
 - SyncBailHook,
 - SyncWaterfallHook,
 - SyncLoopHook,
 - AsyncParallelHook,
 - AsyncParallelBailHook,
 - AsyncSeriesHook,
 - AsyncSeriesBailHook,
 - AsyncSeriesWaterfallHook
 - } = require("tapable");
 
每個(gè) hook 都能被注冊(cè)多次,如何被觸發(fā)取決于 hook 的類(lèi)型
按同步、異步(串行、并行)分類(lèi)
- Sync:只能被同步函數(shù)注冊(cè),如 myHook.tap()
 - AsyncSeries:可以被同步的,基于回調(diào)的,基于 promise 的函數(shù)注冊(cè),如 myHook.tap(),myHook.tapAsync() , myHook.tapPromise()。執(zhí)行順序?yàn)榇?/li>
 - AsyncParallel:可以被同步的,基于回調(diào)的,基于 promise 的函數(shù)注冊(cè),如 myHook.tap(),myHook.tapAsync() , myHook.tapPromise()。執(zhí)行順序?yàn)椴⑿?/li>
 
按執(zhí)行模式分類(lèi)
- Basic:執(zhí)行每一個(gè)事件函數(shù),不關(guān)心函數(shù)的返回值
 
- Bail:執(zhí)行每一個(gè)事件函數(shù),遇到第一個(gè)結(jié)果 result !== undefined 則返回,不再繼續(xù)執(zhí)行
 
- Waterfall:如果前一個(gè)事件函數(shù)的結(jié)果 result !== undefined,則 result 會(huì)作為后一個(gè)事件函數(shù)的第一個(gè)參數(shù)
 
- Loop:不停的循環(huán)執(zhí)行事件函數(shù),直到所有函數(shù)結(jié)果 result === undefined
 
使用方式
Hook 類(lèi)
使用簡(jiǎn)單來(lái)說(shuō)就是下面步驟
- 實(shí)例化構(gòu)造函數(shù) Hook
 - 注冊(cè)(一次或者多次)
 - 執(zhí)行(傳入?yún)?shù))
 - 如果有需要還可以增加對(duì)整個(gè)流程(包括注冊(cè)和執(zhí)行)的監(jiān)聽(tīng)-攔截器
 
以最簡(jiǎn)單的 SyncHook 為例:
- // 簡(jiǎn)單來(lái)說(shuō)就是實(shí)例化 Hooks 類(lèi)
 - // 接收一個(gè)可選參數(shù),參數(shù)是一個(gè)參數(shù)名的字符串?dāng)?shù)組
 - const hook = new SyncHook(["arg1", "arg2", "arg3"]);
 - // 注冊(cè)
 - // 第一個(gè)入?yún)樽?cè)名
 - // 第二個(gè)為注冊(cè)回調(diào)方法
 - hook.tap("1", (arg1, arg2, arg3) => {
 - console.log(1, arg1, arg2, arg3);
 - return 1;
 - });
 - hook.tap("2", (arg1, arg2, arg3) => {
 - console.log(2, arg1, arg2, arg3);
 - return 2;
 - });
 - hook.tap("3", (arg1, arg2, arg3) => {
 - console.log(3, arg1, arg2, arg3);
 - return 3;
 - });
 - // 執(zhí)行
 - // 執(zhí)行順序則是根據(jù)這個(gè)實(shí)例類(lèi)型來(lái)決定的
 - hook.call("a", "b", "c");
 - //------輸出------
 - // 先注冊(cè)先觸發(fā)
 - 1 a b c
 - 2 a b c
 - 3 a b c
 
上面的例子為同步的情況,若注冊(cè)異步則:
- let { AsyncSeriesHook } = require("tapable");
 - let queue = new AsyncSeriesHook(["name"]);
 - console.time("cost");
 - queue.tapPromise("1", function (name) {
 - return new Promise(function (resolve) {
 - setTimeout(function () {
 - console.log(1, name);
 - resolve();
 - }, 1000);
 - });
 - });
 - queue.tapPromise("2", function (name) {
 - return new Promise(function (resolve) {
 - setTimeout(function () {
 - console.log(2, name);
 - resolve();
 - }, 2000);
 - });
 - });
 - queue.tapPromise("3", function (name) {
 - return new Promise(function (resolve) {
 - setTimeout(function () {
 - console.log(3, name);
 - resolve();
 - }, 3000);
 - });
 - });
 - queue.promise("weiyi").then((data) => {
 - console.log(data);
 - console.timeEnd("cost");
 - });
 
HookMap 類(lèi)使用
A HookMap is a helper class for a Map with Hooks
官方推薦將所有的鉤子實(shí)例化在一個(gè)類(lèi)的屬性 hooks 上,如:
- class Car {
 - constructor() {
 - this.hooks = {
 - accelerate: new SyncHook(["newSpeed"]),
 - brake: new SyncHook(),
 - calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
 - };
 - }
 - /* ... */
 - setSpeed(newSpeed) {
 - // following call returns undefined even when you returned values
 - this.hooks.accelerate.call(newSpeed);
 - }
 - }
 
注冊(cè)&執(zhí)行:
- const myCar = new Car();
 - myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
 - myCar.setSpeed(1)
 
而 HookMap 正是這種推薦寫(xiě)法的一個(gè)輔助類(lèi)。具體使用方法:
- const keyedHook = new HookMap(key => new SyncHook(["arg"]))
 - keyedHook.for("some-key").tap("MyPlugin", (arg) => { /* ... */ });
 - keyedHook.for("some-key").tapAsync("MyPlugin", (arg, callback) => { /* ... */ });
 - keyedHook.for("some-key").tapPromise("MyPlugin", (arg) => { /* ... */ });
 - const hook = keyedHook.get("some-key");
 - if(hook !== undefined) {
 - hook.callAsync("arg", err => { /* ... */ });
 - }
 
MultiHook 類(lèi)使用
A helper Hook-like class to redirect taps to multiple other hooks
相當(dāng)于提供一個(gè)存放一個(gè) hooks 列表的輔助類(lèi):
- const { MultiHook } = require("tapable");
 - this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
 
Tapable 的原理
核心就是通過(guò) Hook 來(lái)進(jìn)行注冊(cè)的回調(diào)存儲(chǔ)和觸發(fā),通過(guò) HookCodeFactory 來(lái)控制注冊(cè)的執(zhí)行流程。
首先來(lái)觀察一下 tapable 的 lib 文件結(jié)構(gòu),核心的代碼都是存放在 lib 文件夾中。其中 index.js 為所有可使用類(lèi)的入口。Hook 和 HookCodeFactory 則是核心類(lèi),主要的作用就是注冊(cè)和觸發(fā)流程。還有兩個(gè)輔助類(lèi) HookMap 和 MultiHook 以及一個(gè)工具類(lèi) util-browser。其余均是以 Hook 和 HookCodeFactory 為基礎(chǔ)類(lèi)衍生的以上分類(lèi)所提及的 9 種 Hooks。整個(gè)結(jié)構(gòu)是非常簡(jiǎn)單清楚的。如圖所示:
接下來(lái)講一下最重要的兩個(gè)類(lèi),也是 tapable 的源碼核心。
Hook
首先看 Hook 的屬性,可以看到屬性中有熟悉的注冊(cè)的方法:tap、tapAsync、tapPromise。執(zhí)行方法:call、promise、callAsync。以及存放所有的注冊(cè)項(xiàng) taps。constructor 的入?yún)⒕褪敲總€(gè)鉤子實(shí)例化時(shí)的入?yún)ⅰ膶傩陨暇湍軌蛑朗?Hook 類(lèi)為繼承它的子類(lèi)提供了最基礎(chǔ)的注冊(cè)和執(zhí)行的方法
- class Hook {
 - constructor(args = [], name = undefined) {
 - this._args = args;
 - this.name = name;
 - this.taps = [];
 - this.interceptors = [];
 - this._call = CALL_DELEGATE;
 - this.call = CALL_DELEGATE;
 - this._callAsync = CALL_ASYNC_DELEGATE;
 - this.callAsync = CALL_ASYNC_DELEGATE;
 - this._promise = PROMISE_DELEGATE;
 - this.promise = PROMISE_DELEGATE;
 - this._x = undefined;
 - this.compile = this.compile;
 - this.tap = this.tap;
 - this.tapAsync = this.tapAsync;
 - this.tapPromise = this.tapPromise;
 - }
 - ...
 - }
 
那么 Hook 類(lèi)是如何收集注冊(cè)項(xiàng)的?如代碼所示:
- class Hook {
 - ...
 - tap(options, fn) {
 - this._tap("sync", options, fn);
 - }
 - tapAsync(options, fn) {
 - this._tap("async", options, fn);
 - }
 - tapPromise(options, fn) {
 - this._tap("promise", options, fn);
 - }
 - _tap(type, options, fn) {
 - if (typeof options === "string") {
 - options = {
 - name: options.trim()
 - };
 - } else if (typeof options !== "object" || options === null) {
 - throw new Error("Invalid tap options");
 - }
 - if (typeof options.name !== "string" || options.name === "") {
 - throw new Error("Missing name for tap");
 - }
 - if (typeof options.context !== "undefined") {
 - deprecateContext();
 - }
 - // 合并參數(shù)
 - options = Object.assign({ type, fn }, options);
 - // 執(zhí)行注冊(cè)的 interceptors 的 register 監(jiān)聽(tīng),并返回執(zhí)行后的 options
 - options = this._runRegisterInterceptors(options);
 - // 收集到 taps 中
 - this._insert(options);
 - }
 - _runRegisterInterceptors(options) {
 - for (const interceptor of this.interceptors) {
 - if (interceptor.register) {
 - const newOptions = interceptor.register(options);
 - if (newOptions !== undefined) {
 - options = newOptions;
 - }
 - }
 - }
 - return options;
 - }
 - ...
 - }
 
可以看到三種注冊(cè)的方法都是通過(guò)_tap 來(lái)實(shí)現(xiàn)的,只是傳入的 type 不同。_tap 主要做了兩件事。
- 執(zhí)行 interceptor.register,并返回 options
 - 收集注冊(cè)項(xiàng)到 this.taps 列表中,同時(shí)根據(jù) stage 和 before 排序。(stage 和 before 是注冊(cè)時(shí)的可選參數(shù))
 
收集完注冊(cè)項(xiàng),接下來(lái)就是執(zhí)行這個(gè)流程:
- const CALL_DELEGATE = function(...args) {
 - this.call = this._createCall("sync");
 - return this.call(...args);
 - };
 - const CALL_ASYNC_DELEGATE = function(...args) {
 - this.callAsync = this._createCall("async");
 - return this.callAsync(...args);
 - };
 - const PROMISE_DELEGATE = function(...args) {
 - this.promise = this._createCall("promise");
 - return this.promise(...args);
 - };
 - class Hook {
 - constructor() {
 - ...
 - this._call = CALL_DELEGATE;
 - this.call = CALL_DELEGATE;
 - this._callAsync = CALL_ASYNC_DELEGATE;
 - this.callAsync = CALL_ASYNC_DELEGATE;
 - this._promise = PROMISE_DELEGATE;
 - this.promise = PROMISE_DELEGATE;
 - ...
 - }
 - compile(options) {
 - throw new Error("Abstract: should be overridden");
 - }
 - _createCall(type) {
 - return this.compile({
 - taps: this.taps,
 - interceptors: this.interceptors,
 - args: this._args,
 - type: type
 - });
 - }
 - }
 
執(zhí)行流程可以說(shuō)是殊途同歸,最后都是通過(guò)_createCall 來(lái)返回一個(gè) compile 執(zhí)行后的值。從上文可知,tapable 的執(zhí)行流程有同步,異步串行,異步并行、循環(huán)等,因此 Hook 類(lèi)只提供了一個(gè)抽象方法 compile,那么 compile 具體是怎么樣的呢。這就引出了下一個(gè)核心類(lèi) HookCodeFactory。
HookCodeFactory
見(jiàn)名知意,該類(lèi)是一個(gè)返回 hookCode 的工廠。首先來(lái)看下這個(gè)工廠是如何被使用的。這是其中一種 hook 類(lèi) AsyncSeriesHook 使用方式:
- const HookCodeFactory = require("./HookCodeFactory");
 - class AsyncSeriesHookCodeFactory extends HookCodeFactory {
 - content({ onError, onDone }) {
 - return this.callTapsSeries({
 - onError: (i, err, next, doneBreak) => onError(err) + doneBreak(true),
 - onDone
 - });
 - }
 - }
 - const factory = new AsyncSeriesHookCodeFactory();
 - // options = {
 - // taps: this.taps,
 - // interceptors: this.interceptors,
 - // args: this._args,
 - // type: type
 - // }
 - const COMPILE = function(options) {
 - factory.setup(this, options);
 - return factory.create(options);
 - };
 - function AsyncSeriesHook(args = [], name = undefined) {
 - const hook = new Hook(args, name);
 - hook.constructor = AsyncSeriesHook;
 - hook.compile = COMPILE;
 - ...
 - return hook;
 - }
 
HookCodeFactory 的職責(zé)就是將執(zhí)行代碼賦值給 hook.compile,從而使 hook 得到執(zhí)行能力。來(lái)看看該類(lèi)內(nèi)部運(yùn)轉(zhuǎn)邏輯是這樣的:
- class HookCodeFactory {
 - constructor(config) {
 - this.config = config;
 - this.options = undefined;
 - this._args = undefined;
 - }
 - ...
 - create(options) {
 - ...
 - this.init(options);
 - // type
 - switch (this.options.type) {
 - case "sync": fn = new Function(省略...);break;
 - case "async": fn = new Function(省略...);break;
 - case "promise": fn = new Function(省略...);break;
 - }
 - this.deinit();
 - return fn;
 - }
 - init(options) {
 - this.options = options;
 - this._args = options.args.slice();
 - }
 - deinit() {
 - this.options = undefined;
 - this._args = undefined;
 - }
 - }
 
最終返回給 compile 就是 create 返回的這個(gè) fn,fn 則是通過(guò) new Function()進(jìn)行創(chuàng)建的。那么重點(diǎn)就是這個(gè) new Function 中了。
先了解一下 new Function 的語(yǔ)法
new Function ([arg1[, arg2[, ...argN]],] functionBody)
- arg1, arg2, ... argN:被函數(shù)使用的參數(shù)的名稱(chēng)必須是合法命名的。參數(shù)名稱(chēng)是一個(gè)有效的 JavaScript 標(biāo)識(shí)符的字符串,或者一個(gè)用逗號(hào)分隔的有效字符串的列表;例如“×”,“theValue”,或“a,b”。
 - functionBody:一個(gè)含有包括函數(shù)定義的 JavaScript 語(yǔ)句的字符串。
 
基本用法:
- const sum = new Function('a', 'b', 'return a + b');
 - console.log(sum(2, 6));
 - // expected output: 8
 
使用 Function 構(gòu)造函數(shù)的方法:
- class HookCodeFactory {
 - create() {
 - ...
 - fn = new Function(this.args({...}), code)
 - ...
 - return fn
 - }
 - args({ before, after } = {}) {
 - let allArgs = this._args;
 - if (before) allArgs = [before].concat(allArgs);
 - if (after) allArgs = allArgs.concat(after);
 - if (allArgs.length === 0) {
 - return "";
 - } else {
 - return allArgs.join(", ");
 - }
 - }
 - }
 
這個(gè) this.args()就是返回執(zhí)行時(shí)傳入?yún)?shù)名,為后面 code 提供了對(duì)應(yīng)參數(shù)值。
- fn = new Function(
 - this.args({...}),
 - '"use strict";\n' +
 - this.header() +
 - this.contentWithInterceptors({
 - onError: err => `throw ${err};\n`,
 - onResult: result => `return ${result};\n`,
 - resultReturns: true,
 - onDone: () => "",
 - rethrowIfPossible: true
 - })
 - )
 - header() {
 - let code = "";
 - if (this.needContext()) {
 - code += "var _context = {};\n";
 - } else {
 - code += "var _context;\n";
 - }
 - code += "var _x = this._x;\n";
 - if (this.options.interceptors.length > 0) {
 - code += "var _taps = this.taps;\n";
 - code += "var _interceptors = this.interceptors;\n";
 - }
 - return code;
 - }
 - contentWithInterceptors() {
 - // 由于代碼過(guò)多這邊描述一下過(guò)程
 - // 1. 生成監(jiān)聽(tīng)的回調(diào)對(duì)象如:
 - // {
 - // onError,
 - // onResult,
 - // resultReturns,
 - // onDone,
 - // rethrowIfPossible
 - // }
 - // 2. 執(zhí)行 this.content({...}),入?yún)榈谝徊椒祷氐膶?duì)象
 - ...
 - }
 
而對(duì)應(yīng)的 functionBody 則是通過(guò) header 和 contentWithInterceptors 共同生成的。this.content 則是根據(jù)鉤子類(lèi)型的不同調(diào)用不同的方法如下面代碼則調(diào)用的是 callTapsSeries:
- class SyncHookCodeFactory extends HookCodeFactory {
 - content({ onError, onDone, rethrowIfPossible }) {
 - return this.callTapsSeries({
 - onError: (i, err) => onError(err),
 - onDone,
 - rethrowIfPossible
 - });
 - }
 - }
 
HookCodeFactory 有三種生成 code 的方法:
- // 串行
 - callTapsSeries() {...}
 - // 循環(huán)
 - callTapsLooping() {...}
 - // 并行
 - callTapsParallel() {...}
 - // 執(zhí)行單個(gè)注冊(cè)回調(diào),通過(guò)判斷 sync、async、promise 返回對(duì)應(yīng) code
 - callTap() {...}
 
- 并行(Parallel)原理:并行的情況只有在異步的時(shí)候才發(fā)生,因此執(zhí)行所有的 taps 后,判斷計(jì)數(shù)器是否為 0,為 0 則執(zhí)行結(jié)束回調(diào)(計(jì)數(shù)器為 0 有可能是因?yàn)?taps 全部執(zhí)行完畢,有可能是因?yàn)榉祷刂挡粸?undefined,手動(dòng)設(shè)置為 0)
 - 循環(huán)(Loop)原理:生成 do{}while(__loop)的代碼,將執(zhí)行后的值是否為 undefined 賦值給_loop,從而來(lái)控制循環(huán)
 - 串行:就是按照 taps 的順序來(lái)生成執(zhí)行的代碼
 - callTap:執(zhí)行單個(gè)注冊(cè)回調(diào)
 
- sync:按照順序執(zhí)行
 
- var _fn0 = _x[0];
 - _fn0(arg1, arg2, arg3);
 - var _fn1 = _x[1];
 - _fn1(arg1, arg2, arg3);
 - var _fn2 = _x[2];
 - _fn2(arg1, arg2, arg3);
 
- async 原理:將單個(gè) tap 封裝成一個(gè)_next[index]函數(shù),當(dāng)前一個(gè)函數(shù)執(zhí)行完成即調(diào)用了 callback,則會(huì)繼續(xù)執(zhí)行下一個(gè)_next[index]函數(shù),如生成如下 code:
 
- function _next1() {
 - var _fn2 = _x[2];
 - _fn2(name, (function (_err2) {
 - if (_err2) {
 - _callback(_err2);
 - } else {
 - _callback();
 - }
 - }));
 - }
 - function _next0() {
 - var _fn1 = _x[1];
 - _fn1(name, (function (_err1) {
 - if (_err1) {
 - _callback(_err1);
 - } else {
 - _next1();
 - }
 - }));
 - }
 - var _fn0 = _x[0];
 - _fn0(name, (function (_err0) {
 - if (_err0) {
 - _callback(_err0);
 - } else {
 - _next0();
 - }
 - }));
 
- promise:將單個(gè) tap 封裝成一個(gè)_next[index]函數(shù),當(dāng)前一個(gè)函數(shù)執(zhí)行完成即調(diào)用了 promise.then(),then 中則會(huì)繼續(xù)執(zhí)行下一個(gè)_next[index]函數(shù),如生成如下 code:
 
- function _next1() {
 - var _fn2 = _x[2];
 - var _hasResult2 = false;
 - var _promise2 = _fn2(name);
 - if (!_promise2 || !_promise2.then)
 - throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise2 + ')');
 - _promise2.then((function (_result2) {
 - _hasResult2 = true;
 - _resolve();
 - }), function (_err2) {
 - if (_hasResult2) throw _err2;
 - _error(_err2);
 - });
 - }
 - function _next0() {
 - var _fn1 = _x[1];
 - var _hasResult1 = false;
 - var _promise1 = _fn1(name);
 - if (!_promise1 || !_promise1.then)
 - throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise1 + ')');
 - _promise1.then((function (_result1) {
 - _hasResult1 = true;
 - _next1();
 - }), function (_err1) {
 - if (_hasResult1) throw _err1;
 - _error(_err1);
 - });
 - }
 - var _fn0 = _x[0];
 - var _hasResult0 = false;
 - var _promise0 = _fn0(name);
 - if (!_promise0 || !_promise0.then)
 - throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise0 + ')');
 - _promise0.then((function (_result0) {
 - _hasResult0 = true;
 - _next0();
 - }), function (_err0) {
 - if (_hasResult0) throw _err0;
 - _error(_err0);
 - });
 
將以上的執(zhí)行順序以及執(zhí)行方式來(lái)進(jìn)行組合,就得到了現(xiàn)在的 9 種 Hook 類(lèi)。若后續(xù)需要更多的模式只需要增加執(zhí)行順序或者執(zhí)行方式就能夠完成拓展。
如圖所示:
如何助力 webpack
插件可以使用 tapable 對(duì)外暴露的方法向 webpack 中注入自定義構(gòu)建的步驟,這些步驟將在構(gòu)建過(guò)程中觸發(fā)。
webpack 將整個(gè)構(gòu)建的步驟生成一個(gè)一個(gè) hook 鉤子(即 tapable 的 9 種 hook 類(lèi)型的實(shí)例),存儲(chǔ)在 hooks 的對(duì)象里。插件可以通過(guò) Compiler 或者 Compilation 訪問(wèn)到對(duì)應(yīng)的 hook 鉤子的實(shí)例,進(jìn)行注冊(cè)(tap,tapAsync,tapPromise)。當(dāng) webpack 執(zhí)行到相應(yīng)步驟時(shí)就會(huì)通過(guò) hook 來(lái)進(jìn)行執(zhí)行(call, callAsync,promise),從而執(zhí)行注冊(cè)的回調(diào)。以 ConsoleLogOnBuildWebpackPlugin 自定義插件為例:
- const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
 - class ConsoleLogOnBuildWebpackPlugin {
 - apply(compiler) {
 - compiler.hooks.run.tap(pluginName, (compilation) => {
 - console.log('webpack 構(gòu)建過(guò)程開(kāi)始!');
 - });
 - }
 - }
 - module.exports = ConsoleLogOnBuildWebpackPlugin;
 
可以看到在 apply 中通過(guò) compiler 的 hooks 注冊(cè)(tap)了在 run 階段時(shí)的回調(diào)。從 Compiler 類(lèi)中可以了解到在 hooks 對(duì)象中對(duì) run 屬性賦值 AsyncSeriesHook 的實(shí)例,并在執(zhí)行的時(shí)候通過(guò) this.hooks.run.callAsync 觸發(fā)了已注冊(cè)的對(duì)應(yīng)回調(diào):
- class Compiler {
 - constructor(context) {
 - this.hooks = Object.freeze({
 - ...
 - run: new AsyncSeriesHook(["compiler"]),
 - ...
 - })
 - }
 - run() {
 - ...
 - const run = () => {
 - this.hooks.beforeRun.callAsync(this, err => {
 - if (err) return finalCallback(err);
 - this.hooks.run.callAsync(this, err => {
 - if (err) return finalCallback(err);
 - this.readRecords(err => {
 - if (err) return finalCallback(err);
 - this.compile(onCompiled);
 - });
 - });
 - });
 - };
 - ...
 - }
 - }
 
如圖所示,為該自定義插件的執(zhí)行過(guò)程:
總結(jié)
- tapable 對(duì)外暴露 9 種 hook 鉤子,核心方法是注冊(cè)、執(zhí)行、攔截器
 - tapable 實(shí)現(xiàn)方式就是根據(jù)鉤子類(lèi)型以及注冊(cè)類(lèi)型來(lái)拼接字符串傳入 Function 構(gòu)造函數(shù)創(chuàng)建一個(gè)新的 Function 對(duì)象
 - webpack 通過(guò) tapable 來(lái)對(duì)整個(gè)構(gòu)建步驟進(jìn)行了流程化的管理。實(shí)現(xiàn)了對(duì)每個(gè)構(gòu)建步驟都能進(jìn)行靈活定制化需求。
 
參考資料
[1]webpack 官方文檔中對(duì)于 plugin 的介紹:
https://webpack.docschina.org/concepts/plugins/
[2]tapable 相關(guān)介紹:
http://www.zhufengpeixun.com/grow/html/103.7.webpack-tapable.html
[3]tabpable 源碼:
https://github.com/webpack/tapable
[4]webpack 源碼:
https://github.com/webpack/webpack
























 
 
 









 
 
 
 