Introduction
Webpack 作为主流的打包工具,提供了自定义扩展的loader
和plugin
,丰富了周边生态。如果想自己写一个plugin
,需要对相关的hooks
有所了解,而 webpack 对hooks
的实现则是建立在tapable
这个库上的,本篇文章希望通过对tapable
源码的梳理,以加深webpack
的相关知识以及部分设计模式的理解。
How to use tapable
tapable
和常见发布订阅模式实现的代码用法基本一致。基本的SyncHook
使用如下所示:
1 2 3 4 5 6 7 8 9 10
| const { SyncHook } = require('tapable');
const hook = new SyncHook(['name']);
hook.tap('plugin1', name => { console.log(name); });
hook.call('xbrave');
|
Hook
Hook类作为其它 Hook 类的基类,整体上把发布订阅的内容抽象了出来,整体代码不多,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
| 'use strict';
const util = require('util');
const deprecateContext = util.deprecate(() => {}, 'Hook.context is deprecated and will be removed');
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(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; }
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, }); }
_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); options = this._runRegisterInterceptors(options); this._insert(options); }
tap(options, fn) { this._tap('sync', options, fn); }
tapAsync(options, fn) { this._tap('async', options, fn); }
tapPromise(options, fn) { this._tap('promise', options, fn); }
_runRegisterInterceptors(options) { for (const interceptor of this.interceptors) { if (interceptor.register) { const newOptions = interceptor.register(options); if (newOptions !== undefined) { options = newOptions; } } } return options; }
withOptions(options) { const mergeOptions = opt => Object.assign({}, options, typeof opt === 'string' ? { name: opt } : opt);
return { name: this.name, tap: (opt, fn) => this.tap(mergeOptions(opt), fn), tapAsync: (opt, fn) => this.tapAsync(mergeOptions(opt), fn), tapPromise: (opt, fn) => this.tapPromise(mergeOptions(opt), fn), intercept: interceptor => this.intercept(interceptor), isUsed: () => this.isUsed(), withOptions: opt => this.withOptions(mergeOptions(opt)), }; }
isUsed() { return this.taps.length > 0 || this.interceptors.length > 0; }
intercept(interceptor) { this._resetCompilation(); this.interceptors.push(Object.assign({}, interceptor)); if (interceptor.register) { for (let i = 0; i < this.taps.length; i++) { this.taps[i] = interceptor.register(this.taps[i]); } } }
_resetCompilation() { this.call = this._call; this.callAsync = this._callAsync; this.promise = this._promise; }
_insert(item) { this._resetCompilation(); let before; if (typeof item.before === 'string') { before = new Set([item.before]); } else if (Array.isArray(item.before)) { before = new Set(item.before); } let stage = 0; if (typeof item.stage === 'number') { stage = item.stage; } let i = this.taps.length; while (i > 0) { i--; const x = this.taps[i]; this.taps[i + 1] = x; const xStage = x.stage || 0; if (before) { if (before.has(x.name)) { before.delete(x.name); continue; } if (before.size > 0) { continue; } } if (xStage > stage) { continue; } i++; break; } this.taps[i] = item; } }
Object.setPrototypeOf(Hook.prototype, null);
module.exports = Hook;
|
init
这里需要注意的是this.call
,this.callAsync
等属性初始化的时候指向的是一个函数,主要是考虑到其它类继承时的初始化问题,其它类可以自行实现this.compile
方法,同时compile
初始化为:
1 2 3
| compile(options) { throw new Error("Abstract: should be overridden"); }
|
如果子类不自行实现compile
则会报错
subscribe
内部的_tap()
方法主要作用是整合传入的options
,然后调用内部的this._insert(options)
方法,将相关回调注册到内部的this.taps
,this.insert()
内部还针对options
内的stage
和before
做了一次排序,关于stage
和before
的作用有兴趣的可以了解一下ref
HookCodeFactory
HookCodeFactory的主要作用是针对不同类型的 Hook,相应的动态生成所需要的compile()
函数。
setup()
注意到Hook
类初始化时的_x
并没有指定内容,这里对内部的_x
重新赋值:
1 2 3
| setup(instance, options) { instance._x = options.taps.map(t => t.fn); }
|
new Function()
一般来说,js 中创建函数一般都是函数声明和函数表达式居多,但是还有其他不太常用的方法, 比如new Function()
,示例如下:
1 2 3 4
| const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));
|
上述的参数'a'
和'b'
可以换为'a,b'
即以英文逗号分割的字符串作为多个参数传入。以new Function()
形式声明函数的好处是可以使用字符串动态创建函数,但是可读性比较差,日常开发中的代码不建议这么做。
create()
回到tapable
源码中, create()
方法就是用来动态生成函数的,一共有三种类型,sync
,async
,promise
本篇文章主要关注sync
类型,精简后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| create(options) { this.init(options); let fn; switch (this.options.type) { case "sync": 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 }) ); break; } this.deinit(); return fn; }
|
可以看到create()
方法最主要的还是调用new Function()
来动态创建fn
并返回
SyncHook
基于以上的基础,我们来看一下SyncHook是如何实现的。
SyncHook
的源码比较简单,只有几十行,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| 'use strict';
const Hook = require('./Hook'); const HookCodeFactory = require('./HookCodeFactory');
class SyncHookCodeFactory extends HookCodeFactory { content({ onError, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible, }); } }
const factory = new SyncHookCodeFactory();
const TAP_ASYNC = () => { throw new Error('tapAsync is not supported on a SyncHook'); };
const TAP_PROMISE = () => { throw new Error('tapPromise is not supported on a SyncHook'); };
const COMPILE = function (options) { factory.setup(this, options); return factory.create(options); };
function SyncHook(args = [], name = undefined) { const hook = new Hook(args, name); hook.constructor = SyncHook; hook.tapAsync = TAP_ASYNC; hook.tapPromise = TAP_PROMISE; hook.compile = COMPILE; return hook; }
SyncHook.prototype = null;
module.exports = SyncHook;
|
整体顺序为:
SyncHookCodeFactory
类继承HookCodeFactory
基类,并自行实现content()
方法用于动态生成代码
SyncHook
类继承Hook
基类,并自行实现tapAsync()
,tapPromise()
与compile()
方法用于后续调用
SyncHook.prototype = null;
将prototype
指向null
TODO: why?
关于订阅tap
这块的内容,在之前讲Hook
基类的时候描述过,这里不再赘述,这里主要关注点在如何动态生成call()
方法。
content()
content()主要是生成最终函数的主要内容,相关方法及其后续调用顺序比较深,整体的调用顺序为:
HookCodeFactory.create()
-> HookCodeFactory.contentWithInterceptors()
-> SyncHookCodeFactory.content()
-> HookCodeFactory.callTapsSeries()
-> HookCodeFactory.callTap()
我们重点关注callTap()
的"sync"
部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| case "sync": if (!rethrowIfPossible) { code += `var _hasError${tapIndex} = false;\n`; code += "try {\n"; } if (onResult) { code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })});\n`; } else { code += `_fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })});\n`; } if (!rethrowIfPossible) { code += "} catch(_err) {\n"; code += `_hasError${tapIndex} = true;\n`; code += onError("_err"); code += "}\n"; code += `if(!_hasError${tapIndex}) {\n`; } if (onResult) { code += onResult(`_result${tapIndex}`); } if (onDone) { code += onDone(); } if (!rethrowIfPossible) { code += "}\n"; } break;
|
这里是将每个之前tap
注册的函数从this._x
拿出来,依次赋值给_fn
并执行,生成的字符串类似于
1 2 3 4
| var _fn0 = _x[0]; _fn0(); var _fn1 = _x[1]; _fn1();
|
至于为什么不用 for 循环,应该是考虑到简单可扩展
回到之前的setup()
部分,为什么要执行instance._x = options.taps.map(t => t.fn);
,和header()
放在一起看就比较明晰了,
header()
相关的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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; }
|
可以看出来主要就是生成var _x = this._x;
, 而这个_x
就是callTap()
中用到的_x
,
最终header()
和content()
及其相关的方法生成的结果类似如下形式:
1 2 3 4 5 6 7
| 'use strict'; var _context; var _x = this._x; var _fn0 = _x[0]; _fn0(); var _fn1 = _x[1]; _fn1();
|
再通过new Function()
最终动态生成了SyncHook
的call
方法
Summary
tapable
主要基于Hook
类的发布订阅模式,以及HookCodeFactory
类的工厂模式来抽象代码,这两个类抽象比较高,搞懂这两个类后续就比较好理解了
tapable
基于new Function()
来动态创建call
,callAsync
,promise
方法
Reference
MDN Function() constructor
Query regarding coding paradigm used in this library