構建專欄系列目錄入口html
胡寧:微醫前端技術部平臺支撐組,最近是一陣信奉快樂的風~前端
tapable 是一個相似於 Node.js 中的 EventEmitter 的庫,但更專一於自定義事件的觸發和處理。webpack 經過 tapable 將實現與流程解耦,全部具體實現經過插件的形式存在。webpack
本質上,webpack 是一個用於現代 JavaScript 應用程序的 靜態模塊打包工具。當 webpack 處理應用程序時,它會在內部構建一個 依賴圖(dependency graph),此依賴圖對應映射到項目所需的每一個模塊,並生成一個或多個 bundle。git
插件(plugin)是 webpack 的支柱功能。webpack 自身也是構建於你在 webpack 配置中用到的相同的插件系統之上。github
webpack 本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是 Tapable。webpack 中最核心的負責編譯的 Compiler 和負責建立 bundle 的 Compilation 都是 Tapable 的實例(webpack5 前)。webpack5 以後是經過定義屬性名爲 hooks 來調度觸發時機。Tapable 充當的就是一個複雜的發佈訂閱者模式web
以 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 對外暴露了 9 種 Hooks 類。這些 Hooks 類的做用就是經過實例化來建立一個執行流程,並提供註冊和執行方法,Hook 類的不一樣會致使執行流程的不一樣。promise
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
複製代碼
每一個 hook 都能被註冊屢次,如何被觸發取決於 hook 的類型markdown
簡單來講就是下面步驟app
以最簡單的 SyncHook 爲例:
// 簡單來講就是實例化 Hooks 類
// 接收一個可選參數,參數是一個參數名的字符串數組
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
// 註冊
// 第一個入參爲註冊名
// 第二個爲註冊回調方法
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;
});
// 執行
// 執行順序則是根據這個實例類型來決定的
hook.call("a", "b", "c");
//------輸出------
// 先註冊先觸發
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");
});
複製代碼
A HookMap is a helper class for a Map with Hooks
官方推薦將全部的鉤子實例化在一個類的屬性 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);
}
}
複製代碼
註冊&執行:
const myCar = new Car();
myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));
myCar.setSpeed(1)
複製代碼
而 HookMap 正是這種推薦寫法的一個輔助類。具體使用方法:
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 => { /* ... */ });
}
複製代碼
A helper Hook-like class to redirect taps to multiple other hooks
至關於提供一個存放一個 hooks 列表的輔助類:
const { MultiHook } = require("tapable");
this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);
複製代碼
核心就是經過 Hook 來進行註冊的回調存儲和觸發,經過 HookCodeFactory 來控制註冊的執行流程。
首先來觀察一下 tapable 的 lib 文件結構,核心的代碼都是存放在 lib 文件夾中。其中 index.js 爲全部可以使用類的入口。Hook 和 HookCodeFactory 則是核心類,主要的做用就是註冊和觸發流程。還有兩個輔助類 HookMap 和 MultiHook 以及一個工具類 util-browser。其他均是以 Hook 和 HookCodeFactory 爲基礎類衍生的以上分類所說起的 9 種 Hooks。整個結構是很是簡單清楚的。如圖所示:
接下來說一下最重要的兩個類,也是 tapable 的源碼核心。
首先看 Hook 的屬性,能夠看到屬性中有熟悉的註冊的方法:tap、tapAsync、tapPromise。執行方法:call、promise、callAsync。以及存放全部的註冊項 taps。constructor 的入參就是每一個鉤子實例化時的入參。從屬性上就可以知道是 Hook 類爲繼承它的子類提供了最基礎的註冊和執行的方法
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 類是如何收集註冊項的?如代碼所示:
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();
}
// 合併參數
options = Object.assign({ type, fn }, options);
// 執行註冊的 interceptors 的 register 監聽,並返回執行後的 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 來實現的,只是傳入的 type 不一樣。_tap 主要作了兩件事。
收集完註冊項,接下來就是執行這個流程:
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
});
}
}
複製代碼
執行流程能夠說是異曲同工,最後都是經過_createCall 來返回一個 compile 執行後的值。從上文可知,tapable 的執行流程有同步,異步串行,異步並行、循環等,所以 Hook 類只提供了一個抽象方法 compile,那麼 compile 具體是怎麼樣的呢。這就引出了下一個核心類 HookCodeFactory。
見名知意,該類是一個返回 hookCode 的工廠。首先來看下這個工廠是如何被使用的。這是其中一種 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 的職責就是將執行代碼賦值給 hook.compile,從而使 hook 獲得執行能力。來看看該類內部運轉邏輯是這樣的:
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 返回的這個 fn,fn 則是經過 new Function()進行建立的。那麼重點就是這個 new Function 中了。
先了解一下 new Function 的語法
new Function ([arg1[, arg2[, ...argN]],] functionBody)
基本用法:
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));
// expected output: 8
複製代碼
使用 Function 構造函數的方法:
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(", ");
}
}
}
複製代碼
這個 this.args()就是返回執行時傳入參數名,爲後面 code 提供了對應參數值。
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. 生成監聽的回調對象如:
// {
// onError,
// onResult,
// resultReturns,
// onDone,
// rethrowIfPossible
// }
// 2. 執行 this.content({...}),入參爲第一步返回的對象
...
}
複製代碼
而對應的 functionBody 則是經過 header 和 contentWithInterceptors 共同生成的。this.content 則是根據鉤子類型的不一樣調用不一樣的方法以下面代碼則調用的是 callTapsSeries:
class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}
複製代碼
HookCodeFactory 有三種生成 code 的方法:
// 串行
callTapsSeries() {...}
// 循環
callTapsLooping() {...}
// 並行
callTapsParallel() {...}
// 執行單個註冊回調,經過判斷 sync、async、promise 返回對應 code
callTap() {...}
複製代碼
var _fn0 = _x[0];
_fn0(arg1, arg2, arg3);
var _fn1 = _x[1];
_fn1(arg1, arg2, arg3);
var _fn2 = _x[2];
_fn2(arg1, arg2, arg3);
複製代碼
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();
}
}));
複製代碼
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);
});
複製代碼
將以上的執行順序以及執行方式來進行組合,就獲得瞭如今的 9 種 Hook 類。若後續須要更多的模式只須要增長執行順序或者執行方式就可以完成拓展。
如圖所示:
插件可使用 tapable 對外暴露的方法向 webpack 中注入自定義構建的步驟,這些步驟將在構建過程當中觸發。
webpack 將整個構建的步驟生成一個一個 hook 鉤子(即 tapable 的 9 種 hook 類型的實例),存儲在 hooks 的對象裏。插件能夠經過 Compiler 或者 Compilation 訪問到對應的 hook 鉤子的實例,進行註冊(tap,tapAsync,tapPromise)。當 webpack 執行到相應步驟時就會經過 hook 來進行執行(call, callAsync,promise),從而執行註冊的回調。以 ConsoleLogOnBuildWebpackPlugin 自定義插件爲例:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('webpack 構建過程開始!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
複製代碼
能夠看到在 apply 中經過 compiler 的 hooks 註冊(tap)了在 run 階段時的回調。從 Compiler 類中能夠了解到在 hooks 對象中對 run 屬性賦值 AsyncSeriesHook 的實例,並在執行的時候經過 this.hooks.run.callAsync 觸發了已註冊的對應回調:
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);
});
});
});
};
...
}
}
複製代碼
如圖所示,爲該自定義插件的執行過程:
若有意見,歡迎一鍵素質三連,寶~。
[1]webpack 官方文檔中對於 plugin 的介紹: webpack.docschina.org/concepts/pl…
[2]tapable 相關介紹:www.zhufengpeixun.com/grow/html/1…
[3]tabpable 源碼:github.com/webpack/tap…
[4]webpack 源碼:github.com/webpack/web…