构建专栏系列目录入口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…