從使用到原理,吃透Tapable

胡寧:微醫(yī)前端技術(shù)部平臺(tái)支撐組,最近是一陣信奉快樂的風(fēng)~
tapable 是一個(gè)類似于 Node.js 中的 EventEmitter 的庫,但更專注于自定義事件的觸發(fā)和處理。webpack 通過 tapable 將實(shí)現(xiàn)與流程解耦,所有具體實(shí)現(xiàn)通過插件的形式存在。
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è) 依賴圖(dependency graph),此依賴圖對應(yīng)映射到項(xiàng)目所需的每個(gè)模塊,并生成一個(gè)或多個(gè) bundle。
- webpack 的重要模塊
- 入口(entry)
- 輸出(output)
- loader(對模塊的源代碼進(jìn)行轉(zhuǎn)換)
- plugin(webpack 構(gòu)建流程中的特定時(shí)機(jī)注入擴(kuò)展邏輯來改變構(gòu)建結(jié)果或做你想要的事)
插件(plugin)是 webpack 的支柱功能。webpack 自身也是構(gòu)建于你在 webpack 配置中用到的相同的插件系統(tǒng)之上。
- webpack 的構(gòu)建流程
webpack 本質(zhì)上是一種事件流的機(jī)制,它的工作流程就是將各個(gè)插件串聯(lián)起來,而實(shí)現(xiàn)這一切的核心就是 Tapable。webpack 中最核心的負(fù)責(zé)編譯的 Compiler 和負(fù)責(zé)創(chuàng)建 bundle 的 Compilation 都是 Tapable 的實(shí)例(webpack5 前)。webpack5 之后是通過定義屬性名為 hooks 來調(diào)度觸發(fā)時(shí)機(jī)。Tapable 充當(dāng)?shù)木褪且粋€(gè)復(fù)雜的發(fā)布訂閱者模式
以 Compiler 為例:
//?webpack5?前,通過繼承
...
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 的使用姿勢tapable 對外暴露了 9 種 Hooks 類。這些 Hooks 類的作用就是通過實(shí)例化來創(chuàng)建一個(gè)執(zhí)行流程,并提供注冊和執(zhí)行方法,Hook 類的不同會(huì)導(dǎo)致執(zhí)行流程的不同。
const?{
?SyncHook,
?SyncBailHook,
?SyncWaterfallHook,
?SyncLoopHook,
?AsyncParallelHook,
?AsyncParallelBailHook,
?AsyncSeriesHook,
?AsyncSeriesBailHook,
?AsyncSeriesWaterfallHook
?}?=?require("tapable");
每個(gè) hook 都能被注冊多次,如何被觸發(fā)取決于 hook 的類型
按同步、異步(串行、并行)分類
- Sync:只能被同步函數(shù)注冊,如 myHook.tap()
- AsyncSeries:可以被同步的,基于回調(diào)的,基于 promise 的函數(shù)注冊,如 myHook.tap(),myHook.tapAsync() , myHook.tapPromise()。執(zhí)行順序?yàn)榇?/li>
- AsyncParallel:可以被同步的,基于回調(diào)的,基于 promise 的函數(shù)注冊,如 myHook.tap(),myHook.tapAsync() , myHook.tapPromise()。執(zhí)行順序?yàn)椴⑿?/li>
Untitled.png按執(zhí)行模式分類
- Basic:執(zhí)行每一個(gè)事件函數(shù),不關(guān)心函數(shù)的返回值
Tapable bda4604e3f27488082fd7a2820082dbc.png- Bail:執(zhí)行每一個(gè)事件函數(shù),遇到第一個(gè)結(jié)果 result !== undefined 則返回,不再繼續(xù)執(zhí)行
_(1).png- Waterfall:如果前一個(gè)事件函數(shù)的結(jié)果 result !== undefined,則 result 會(huì)作為后一個(gè)事件函數(shù)的第一個(gè)參數(shù)
_(2).png- Loop:不停的循環(huán)執(zhí)行事件函數(shù),直到所有函數(shù)結(jié)果 result === undefined
_(4).png
Untitled 1.png使用方式
Hook 類使用
簡單來說就是下面步驟
- 實(shí)例化構(gòu)造函數(shù) Hook
- 注冊(一次或者多次)
- 執(zhí)行(傳入?yún)?shù))
- 如果有需要還可以增加對整個(gè)流程(包括注冊和執(zhí)行)的監(jiān)聽-攔截器
以最簡單的 SyncHook 為例:
//?簡單來說就是實(shí)例化?Hooks?類
//?接收一個(gè)可選參數(shù),參數(shù)是一個(gè)參數(shù)名的字符串?dāng)?shù)組
const?hook?=?new?SyncHook(["arg1",?"arg2",?"arg3"]);
//?注冊
//?第一個(gè)入?yún)樽悦?/span>
//?第二個(gè)為注冊回調(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í)例類型來決定的
hook.call("a",?"b",?"c");
//------輸出------
//?先注冊先觸發(fā)
1?a?b?c
2?a?b?c
3?a?b?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 類使用
A HookMap is a helper class for a Map with Hooks
官方推薦將所有的鉤子實(shí)例化在一個(gè)類的屬性 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);
?}
}
注冊&執(zhí)行:
const?myCar?=?new?Car();
myCar.hooks.accelerate.tap("LoggerPlugin",?newSpeed?=>?console.log(`Accelerating?to?${newSpeed}`));
myCar.setSpeed(1)
而 HookMap 正是這種推薦寫法的一個(gè)輔助類。具體使用方法:
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 類使用
A helper Hook-like class to redirect taps to multiple other hooks
相當(dāng)于提供一個(gè)存放一個(gè) hooks 列表的輔助類:
const?{?MultiHook?}?=?require("tapable");
this.hooks.allHooks?=?new?MultiHook([this.hooks.hookA,?this.hooks.hookB]);
Tapable 的原理核心就是通過 Hook 來進(jìn)行注冊的回調(diào)存儲(chǔ)和觸發(fā),通過 HookCodeFactory 來控制注冊的執(zhí)行流程。
首先來觀察一下 tapable 的 lib 文件結(jié)構(gòu),核心的代碼都是存放在 lib 文件夾中。其中 index.js 為所有可使用類的入口。Hook 和 HookCodeFactory 則是核心類,主要的作用就是注冊和觸發(fā)流程。還有兩個(gè)輔助類 HookMap 和 MultiHook 以及一個(gè)工具類 util-browser。其余均是以 Hook 和 HookCodeFactory 為基礎(chǔ)類衍生的以上分類所提及的 9 種 Hooks。整個(gè)結(jié)構(gòu)是非常簡單清楚的。如圖所示:
Untitled 2.png接下來講一下最重要的兩個(gè)類,也是 tapable 的源碼核心。
Hook
首先看 Hook 的屬性,可以看到屬性中有熟悉的注冊的方法:tap、tapAsync、tapPromise。執(zhí)行方法:call、promise、callAsync。以及存放所有的注冊項(xiàng) taps。constructor 的入?yún)⒕褪敲總€(gè)鉤子實(shí)例化時(shí)的入?yún)?。從屬性上就能夠知道?Hook 類為繼承它的子類提供了最基礎(chǔ)的注冊和執(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 類是如何收集注冊項(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í)行注冊的?interceptors?的?register?監(jiān)聽,并返回執(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;
?}
?...
}
可以看到三種注冊的方法都是通過_tap 來實(shí)現(xiàn)的,只是傳入的 type 不同。_tap 主要做了兩件事。
- 執(zhí)行 interceptor.register,并返回 options
- 收集注冊項(xiàng)到 this.taps 列表中,同時(shí)根據(jù) stage 和 before 排序。(stage 和 before 是注冊時(shí)的可選參數(shù))
收集完注冊項(xiàng),接下來就是執(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í)行流程可以說是殊途同歸,最后都是通過_createCall 來返回一個(gè) compile 執(zhí)行后的值。從上文可知,tapable 的執(zhí)行流程有同步,異步串行,異步并行、循環(huán)等,因此 Hook 類只提供了一個(gè)抽象方法 compile,那么 compile 具體是怎么樣的呢。這就引出了下一個(gè)核心類 HookCodeFactory。
HookCodeFactory
見名知意,該類是一個(gè)返回 hookCode 的工廠。首先來看下這個(gè)工廠是如何被使用的。這是其中一種 hook 類 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í)行能力。來看看該類內(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 則是通過 new Function()進(jìn)行創(chuàng)建的。那么重點(diǎn)就是這個(gè) new Function 中了。
先了解一下 new Function 的語法
new Function ([arg1[, arg2[, ...argN]],] functionBody)
- arg1, arg2, ... argN:被函數(shù)使用的參數(shù)的名稱必須是合法命名的。參數(shù)名稱是一個(gè)有效的 JavaScript 標(biāo)識(shí)符的字符串,或者一個(gè)用逗號(hào)分隔的有效字符串的列表;例如“×”,“theValue”,或“a,b”。
- functionBody:一個(gè)含有包括函數(shù)定義的 JavaScript 語句的字符串。
基本用法:
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 提供了對應(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()?{
?//?由于代碼過多這邊描述一下過程
?// 1. 生成監(jiān)聽的回調(diào)對象如:
?//?{
?//??onError,
?//??onResult,
?//??resultReturns,
?//??onDone,
?//??rethrowIfPossible
?//?}
??//?2.?執(zhí)行?this.content({...}),入?yún)榈谝徊椒祷氐膶ο?/span>
?...
}
而對應(yīng)的 functionBody 則是通過 header 和 contentWithInterceptors 共同生成的。this.content 則是根據(jù)鉤子類型的不同調(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è)注冊回調(diào),通過判斷?sync、async、promise?返回對應(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,從而來控制循環(huán)
- 串行:就是按照 taps 的順序來生成執(zhí)行的代碼
- callTap:執(zhí)行單個(gè)注冊回調(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í)行方式來進(jìn)行組合,就得到了現(xiàn)在的 9 種 Hook 類。若后續(xù)需要更多的模式只需要增加執(zhí)行順序或者執(zhí)行方式就能夠完成拓展。
如圖所示:
Tapable bda4604e3f27488082fd7a2820082dbc 1.png如何助力 webpack插件可以使用 tapable 對外暴露的方法向 webpack 中注入自定義構(gòu)建的步驟,這些步驟將在構(gòu)建過程中觸發(fā)。
webpack 將整個(gè)構(gòu)建的步驟生成一個(gè)一個(gè) hook 鉤子(即 tapable 的 9 種 hook 類型的實(shí)例),存儲(chǔ)在 hooks 的對象里。插件可以通過 Compiler 或者 Compilation 訪問到對應(yīng)的 hook 鉤子的實(shí)例,進(jìn)行注冊(tap,tapAsync,tapPromise)。當(dāng) webpack 執(zhí)行到相應(yīng)步驟時(shí)就會(huì)通過 hook 來進(jìn)行執(zhí)行(call, callAsync,promise),從而執(zhí)行注冊的回調(diào)。以 ConsoleLogOnBuildWebpackPlugin 自定義插件為例:
const?pluginName?=?'ConsoleLogOnBuildWebpackPlugin';
class?ConsoleLogOnBuildWebpackPlugin?{
??apply(compiler)?{
????compiler.hooks.run.tap(pluginName,?(compilation)?=>?{
??????console.log('webpack 構(gòu)建過程開始!');
????});
??}
}
module.exports?=?ConsoleLogOnBuildWebpackPlugin;
可以看到在 apply 中通過 compiler 的 hooks 注冊(tap)了在 run 階段時(shí)的回調(diào)。從 Compiler 類中可以了解到在 hooks 對象中對 run 屬性賦值 AsyncSeriesHook 的實(shí)例,并在執(zhí)行的時(shí)候通過 this.hooks.run.callAsync 觸發(fā)了已注冊的對應(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í)行過程:
_(1) 1.png總結(jié)- tapable 對外暴露 9 種 hook 鉤子,核心方法是注冊、執(zhí)行、攔截器
- tapable 實(shí)現(xiàn)方式就是根據(jù)鉤子類型以及注冊類型來拼接字符串傳入 Function 構(gòu)造函數(shù)創(chuàng)建一個(gè)新的 Function 對象
- webpack 通過 tapable 來對整個(gè)構(gòu)建步驟進(jìn)行了流程化的管理。實(shí)現(xiàn)了對每個(gè)構(gòu)建步驟都能進(jìn)行靈活定制化需求。
如有意見,歡迎一鍵素質(zhì)三連,寶~。
參考資料[1]webpack 官方文檔中對于 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
