Webpack 基于 tapable 构建了其复杂庞大的流程管理系统,基于 tapable 的架构不只解耦了流程节点和流程的具体实现,还保证了 Webpack 强大的扩展能力;学习掌握tapable,有助于咱们深刻理解 Webpack。javascript
The tapable package expose many Hook classes,which can be used to create hooks for plugins.java
tapable 提供了一些用于建立插件的钩子类。ios
我的以为 tapable 是一个基于事件的流程管理工具。ajax
tapable于2020.9.18发布了v2.0版本。此文章内容也是基于v2.0版本。axios
tapable有两个基类:Hook和HookCodeFactory。Hook类定义了Hook interface(Hook接口), HookCodeFactoruy类的做用是动态生成一个流程控制函数。生成函数的方式是经过咱们熟悉的New Function(arg,functionBody)。api
tapable会动态生成一个可执行函数来控制钩子函数的执行。咱们以SyncHook的使用来举一个例子,好比咱们有这样的一段代码:数组
// SyncHook使用 import { SyncHook } from '../lib'; const syncHook = new SyncHook(); syncHook.tap('x', () => console.log('x done')); syncHook.tap('y', () => console.log('y done'));
上面的代码只是注册好了钩子函数,要让函数被执行,还须要触发事件(执行调用)promise
syncHook.call();
syncHook.call()在调用时会生成这样的一个动态函数:架构
function anonymous() { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; _fn0(); var _fn1 = _x[1]; _fn1(); }
这个函数的代码很是简单:就是从一个数组中取出函数,依次执行。注意:不一样的调用方式,最终生成的的动态函数是不一样的。若是把调用代码改为:异步
syncHook.callAsync( () => {console.log('all done')} )
那么最终生成的动态函数是这样的:
function anonymous(_callback) { "use strict"; var _context; var _x = this._x; var _fn0 = _x[0]; var _hasError0 = false; try { _fn0(); } catch(_err) { _hasError0 = true; _callback(_err); } if(!_hasError0) { var _fn1 = _x[1]; var _hasError1 = false; try { _fn1(); } catch(_err) { _hasError1 = true; _callback(_err); } if(!_hasError1) { _callback(); } } }
这个动态函数相对于前面的动态函数要复杂一些,但仔细一看,执行逻辑也很是简单:一样是从数组中取出函数,依次执行;只不过此次多了2个逻辑:
经过研究最终生成的动态函数,咱们不难发现:动态函数的模板特性很是突出。前面的例子中,咱们只注册了x,y2个钩子,这个模板保证了当咱们注册任意个钩子时,动态函数也能方便地生成出来,具备很是强的扩展能力。
那么这些动态函数是如何生成的呢?其实Hook的生成流程是同样的。hook.tap只是完成参数准备,真正的动态函数生成是在调用后(水龙头打开后)。完整流程以下:
在tapablev2中,一共提供了12种类型的Hook,接下来,经过梳理Hook怎么执行和Hook完成回调什么时候执行2方面来理解tapable提供的这些Hook类。
钩子函数按次序依次所有执行;若是有Hook回调,则Hook回调在最后执行。
const syncHook = new SyncHook(); syncHook.tap('x', () => console.log('x done')); syncHook.tap('y', () => console.log('y done')); syncHook.callAsync(() => { console.log('all done') }); /* 输出: x done y done all done */
钩子函数按次序执行。若是某一步钩子返回了非undefined,则后面的钩子再也不执行;若是有Hook回调,直接执行Hook回调。
const hook = new SyncBailHook(); hook.tap('x', () => { console.log('x done'); return false; // 返回了非undefined,y不会执行 }); hook.tap('y', () => console.log('y done')); hook.callAsync(() => { console.log('all done') }); /* 输出: x done all done */
钩子函数按次序所有执行。后一个钩子的参数是前一个钩子的返回值。最后执行Hook回调。
const hook = new SyncWaterfallHook(['count']); hook.tap('x', (count) => { let result = count + 1; console.log('x done', result); return result; }); hook.tap('y', (count) => { let result = count * 2; console.log('y done', result); return result; }); hook.tap('z', (count) => { console.log('z done & show result', count); }); hook.callAsync(5, () => { console.log('all done') }); /* 输出: x done 6 y done 12 z done & show result 12 all done */
钩子函数按次序所有执行。每一步的钩子都会循环执行,直到返回值为undefined,再开始执行下一个钩子。Hook回调最后执行。
const hook = new SyncLoopHook(); let flag = 0; let flag1 = 5; hook.tap('x', () => { flag = flag + 1; if (flag >= 5) { // 执行5次,再执行 y console.log('x done'); return undefined; } else { console.log('x loop'); return true; } }); hook.tap('y', () => { flag1 = flag1 * 2; if (flag1 >= 20) { // 执行2次,再执行 z console.log('y done'); return undefined; } else { console.log('y loop'); return true; } }); hook.tap('z', () => { console.log('z done'); // z直接返回了undefined,因此只执行1次 return undefined; }); hook.callAsync(() => { console.log('all done') }); /* 输出: x loop x loop x loop x loop x done y loop x done y done z done all done */
钩子函数异步并行所有执行。全部钩子的回调返回后,Hook回调才执行。
const hook = new AsyncParallelHook(['arg1']); const start = Date.now(); hook.tapAsync('x', (arg1, callback) => { console.log('x done', arg1); setTimeout(() => { callback(); }, 1000) }); hook.tapAsync('y', (arg1, callback) => { console.log('y done', arg1); setTimeout(() => { callback(); }, 2000) }); hook.tapAsync('z', (arg1, callback) => { console.log('z done', arg1); setTimeout(() => { callback(); }, 3000) }); hook.callAsync(1, () => { console.log(`all done。 耗时:${Date.now() - start}`); }); /* 输出: x done 1 y done 1 z done 1 all done。 耗时:3006 */
钩子函数异步串行所有执行,会保证钩子执行顺序,上一个钩子结束后,下一个才会开始。Hook回调最后执行。
const hook = new AsyncSeriesHook(['arg1']); const start = Date.now(); hook.tapAsync('x', (arg1, callback) => { console.log('x done', ++arg1); setTimeout(() => { callback(); }, 1000) }); hook.tapAsync('y', (arg1, callback) => { console.log('y done', arg1); setTimeout(() => { callback(); }, 2000) }); hook.tapAsync('z', (arg1, callback) => { console.log('z done', arg1); setTimeout(() => { callback(); }, 3000) }); hook.callAsync(1, () => { console.log(`all done。 耗时:${Date.now() - start}`); }); /* 输出: x done 2 y done 1 z done 1 all done。 耗时:6008 */
钩子异步并行执行,即钩子都会执行,但只要有一个钩子返回了非undefined,Hook回调会直接执行。
const hook = new AsyncParallelBailHook(['arg1']); const start = Date.now(); hook.tapAsync('x', (arg1, callback) => { console.log('x done', arg1); setTimeout(() => { callback(); }, 1000) }); hook.tapAsync('y', (arg1, callback) => { console.log('y done', arg1); setTimeout(() => { callback(true); }, 2000) }); hook.tapAsync('z', (arg1, callback) => { console.log('z done', arg1); setTimeout(() => { callback(); }, 3000) }); hook.callAsync(1, () => { console.log(`all done。 耗时:${Date.now() - start}`); }); /* 输出: x done 1 y done 1 z done 1 all done。 耗时:2006 */
钩子函数异步串行执行。但只要有一个钩子返回了非undefined,Hook回调就执行,也就是说有的钩子可能不会执行。
const hook = new AsyncSeriesBailHook(['arg1']); const start = Date.now(); hook.tapAsync('x', (arg1, callback) => { console.log('x done', ++arg1); setTimeout(() => { callback(true); // y 不会执行 }, 1000); }); hook.tapAsync('y', (arg1, callback) => { console.log('y done', arg1); setTimeout(() => { callback(); }, 2000); }); hook.callAsync(1, () => { console.log(`all done。 耗时:${Date.now() - start}`); }); /* 输出: x done 2 all done。 耗时:1006 */
钩子函数异步串行所有执行,上一个钩子返回的参数会传给下一个钩子。Hook回调会在全部钩子回调返回后才执行。
const hook = new AsyncSeriesWaterfallHook(['arg']); const start = Date.now(); hook.tapAsync('x', (arg, callback) => { console.log('x done', arg); setTimeout(() => { callback(null, arg + 1); }, 1000) },); hook.tapAsync('y', (arg, callback) => { console.log('y done', arg); setTimeout(() => { callback(null, true); // 不会阻止 z 的执行 }, 2000) }); hook.tapAsync('z', (arg, callback) => { console.log('z done', arg); callback(); }); hook.callAsync(1, (x, arg) => { console.log(`all done, arg: ${arg}。 耗时:${Date.now() - start}`); }); /* 输出: x done 1 y done 2 z done true all done, arg: true。 耗时:3010 */
钩子函数异步串行所有执行,某一步钩子函数会循环执行到返回非undefined,才会开始下一个钩子。Hook回调会在全部钩子回调完成后执行。
const hook = new AsyncSeriesLoopHook(['arg']); const start = Date.now(); let counter = 0; hook.tapAsync('x', (arg, callback) => { console.log('x done', arg); counter++; setTimeout(() => { if (counter >= 5) { callback(null, undefined); // 开始执行 y } else { callback(null, ++arg); // callback(err, result) } }, 1000) },); hook.tapAsync('y', (arg, callback) => { console.log('y done', arg); setTimeout(() => { callback(null, undefined); }, 2000) }); hook.tapAsync('z', (arg, callback) => { console.log('z done', arg); callback(null, undefined); }); hook.callAsync('AsyncSeriesLoopHook', (x, arg) => { console.log(`all done, arg: ${arg}。 耗时:${Date.now() - start}`); }); /* x done AsyncSeriesLoopHook x done AsyncSeriesLoopHook x done AsyncSeriesLoopHook x done AsyncSeriesLoopHook x done AsyncSeriesLoopHook y done AsyncSeriesLoopHook z done AsyncSeriesLoopHook all done, arg: undefined。 耗时:7014 */
主要做用是Hook分组,方便Hook组批量调用。
const hookMap = new HookMap(() => new SyncHook(['x'])); hookMap.for('key1').tap('p1', function() { console.log('key1-1:', ...arguments); }); hookMap.for('key1').tap('p2', function() { console.log('key1-2:', ...arguments); }); hookMap.for('key2').tap('p3', function() { console.log('key2', ...arguments); }); const hook = hookMap.get('key1'); if( hook !== undefined ) { hook.call('hello', function() { console.log('', ...arguments) }); } /* 输出: key1-1: hello key1-2: hello */
MultiHook主要用于向Hook批量注册钩子函数。
const syncHook = new SyncHook(['x']); const syncLoopHook = new SyncLoopHook(['y']); const mutiHook = new MultiHook([syncHook, syncLoopHook]); // 向多个hook注册同一个函数 mutiHook.tap('plugin', (arg) => { console.log('common plugin', arg); }); // 执行函数 for (const hook of mutiHook.hooks) { hook.callAsync('hello', () => { console.log('hook all done'); }); }
以上Hook又能够抽象为如下几类:
xxxBailHook:根据前一步钩子函数的返回值是不是undefined来决定要不要执行下一步钩子:若是某一步返回了非undefined,则后面的钩子不在执行。
xxxWaterfallHook:上一步钩子函数返回值就是下一步函数的参数。
注意钩子函数返回值判断是和undefined对比,而不是和假值对比(null, false)
Hook也能够按同步、异步划分:
syncXXX:同步钩子
Hook实例默认都有都有tap, tapAsync, tapPromise三个注册钩子回调的方法,不一样注册方法生成的动态函数是不同的。固然也并非全部Hook都支持这几个方法,好比SyncHook不支持tapAsync, tapPromise。
Hook默认有call, callAsync,promise来执行回调。但并非全部Hook都会有这几个方法,好比SyncHook不支持callAsync和promise。
咱们先复习下jQuery.ajax()的常规用法(大概用法是这样,咱不纠结每一个参数都正确):
jQuery.ajax({ url: 'api/request/url', beforeSend: function(config) { return config; // 返回false会取消这次请求发送 }, success: function(data) { // 成功逻辑 } error: function(err) { // 失败逻辑 }, complete: function() { // 成功,失败都会执行的逻辑 } });
jQuery.ajax整个流程作了这么几件事:
同时,咱们借鉴axios的作法,将beforeSend改成transformRequest,加入transformResponse,再加上统一的请求loading和默认的错误处理,这时咱们整个ajax流程以下:
const { SyncHook, AsyncSeriesWaterfallHook } = require('tapable'); class Service { constructor() { this.hooks = { loading: new SyncHook(['show']), transformRequest: new AsyncSeriesWaterfallHook(['config', 'transformFunction']), request: new SyncHook(['config']), transformResponse: new AsyncSeriesWaterfallHook(['config', 'response', 'transformFunction']), success: new SyncHook(['data']), fail: new SyncHook(['config', 'error']), finally: new SyncHook(['config', 'xhr']) }; this.init(); } init() { // 解耦后的任务逻辑 this.hooks.loading.tap('LoadingToggle', (show) => { if (show) { console.log('展现ajax-loading'); } else { console.log('关闭ajax-loading'); } }); this.hooks.transformRequest.tapAsync('DoTransformRequest', ( config, transformFunction= (d) => { d.__transformRequest = true; return d; }, cb ) => { console.log(`transformRequest拦截器:Origin:${JSON.stringify(config)};`); config = transformFunction(config); console.log(`transformRequest拦截器:after:${JSON.stringify(config)};`); cb(null, config); }); this.hooks.transformResponse.tapAsync('DoTransformResponse', ( config, data, transformFunction= (d) => { d.__transformResponse = true; return d; }, cb ) => { console.log(`transformResponse拦截器:Origin:${JSON.stringify(config)};`); data = transformFunction(data); console.log(`transformResponse拦截器:After:${JSON.stringify(data)}`); cb(null, data); }); this.hooks.request.tap('DoRequest', (config) => { console.log(`发送请求配置:${JSON.stringify(config)}`); // 模拟数据返回 const sucData = { code: 0, data: { list: ['X50 Pro', 'IQOO Neo'], user: 'jack' }, message: '请求成功' }; const errData = { code: 100030, message: '未登陆,请从新登陆' }; if (Date.now() % 2 === 0) { this.hooks.transformResponse.callAsync(config, sucData, undefined, () => { this.hooks.success.callAsync(sucData, () => { this.hooks.finally.call(config, sucData); }); }); } else { this.hooks.fail.callAsync(config, errData, () => { this.hooks.finally.call(config, errData); }); } }); } start(config) { this.config = config; /* 经过Hook调用定制串联流程 1. 先 transformRequest 2. 处理 loading 3. 发起 request */ this.hooks.transformRequest.callAsync(this.config, undefined, () => { this.hooks.loading.callAsync(this.config.loading, () => { }); this.hooks.request.call(this.config); }); } } const s = new Service(); s.hooks.success.tap('RenderList', (res) => { const { data } = res; console.log(`列表数据:${JSON.stringify(data.list)}`); }); s.hooks.success.tap('UpdateUserInfo', (res) => { const { data } = res; console.log(`用户信息:${JSON.stringify(data.user)}`); }); s.hooks.fail.tap('HandlerError', (config, error) => { console.log(`请求失败了,config=${JSON.stringify(config)},error=${JSON.stringify(error)}`); }); s.hooks.finally.tap('DoFinally', (config, data) => { console.log(`DoFinally,config=${JSON.stringify(config)},data=${JSON.stringify(data)}`); }); s.start({ base: '/cgi/cms/', loading: true }); /* 成功返回输出: transformRequest拦截器:Origin:{"base":"/cgi/cms/","loading":true}; transformRequest拦截器:after:{"base":"/cgi/cms/","loading":true,"__transformRequest":true}; 展现ajax-loading 发送请求配置:{"base":"/cgi/cms/","loading":true,"__transformRequest":true} transformResponse拦截器:Origin:{"base":"/cgi/cms/","loading":true,"__transformRequest":true}; transformResponse拦截器:After:{"code":0,"data":{"list":["X50 Pro","IQOO Neo"],"user":"jack"},"message":"请求成功","__transformResponse":true} 列表数据:["X50 Pro","IQOO Neo"] 用户信息:"jack" DoFinally,config={"base":"/cgi/cms/","loading":true,"__transformRequest":true},data={"code":0,"data":{"list":["X50 Pro","IQOO Neo"],"user":"jack"},"message":"请求成功","__transformResponse":true} */
上面的代码,咱们能够继续优化:把每一个流程点都抽象成一个独立插件,最后再串联起来。如处理loading展现的独立成LoadingPlugin.js,返回预处理transformResponse独立成TransformResponsePlugin.js,这样咱们可能获得这么一个结构:
这个结构就和大名鼎鼎的Webpack组织插件的形式基本一致了。接下来咱们看看tapable在Webpack中的应用,看一看为何tapable可以称为Webpack基石。
若是你须要强大的流程管理能力,能够考虑基于tapable去作架构设计。
做者:vivo-Ou Fujun