Webpack 基石 tapable 揭秘
Webpack 基于 tapable 构建了其复杂庞大的流程管理系统,基于 tapable 的架构不仅解耦了流程节点和流程的具体实现,还保证了 Webpack 强大的扩展能力;学习掌握tapable,有助于我们深入理解 Webpack。
一、tapable是什么?
The tapable package expose many Hook classes,which can be used to create hooks for plugins.
tapable 提供了一些用于创建插件的钩子类。
个人觉得 tapable 是一个基于事件的流程管理工具。
二、tapable架构原理和执行过程
tapable于2020.9.18发布了v2.0版本。此文章内容也是基于v2.0版本。
2.1 代码架构
tapable有两个基类:Hook和HookCodeFactory。Hook类定义了Hook interface(Hook接口), HookCodeFactoruy类的作用是动态生成一个流程控制函数。生成函数的方式是通过我们熟悉的New Function(arg,functionBody)。
2.2 执行流程
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'));
上面的代码只是注册好了钩子函数,要让函数被执行,还需要触发事件(执行调用)
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只是完成参数准备,真正的动态函数生成是在调用后(水龙头打开后)。完整流程如下:
三、Hook 类型详解
在tapablev2中,一共提供了12种类型的Hook,接下来,通过梳理Hook怎么执行和Hook完成回调何时执行2方面来理解tapable提供的这些Hook类。
3.1 SyncHook
钩子函数按次序依次全部执行;如果有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
*/
3.2 SyncBailHook
钩子函数按次序执行。如果某一步钩子返回了非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
*/
3.3 SyncWaterfallHook
钩子函数按次序全部执行。后一个钩子的参数是前一个钩子的返回值。最后执行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
*/
3.4 SyncLoopHook
钩子函数按次序全部执行。每一步的钩子都会循环执行,直到返回值为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
*/
3.5 AsyncParallelHook
钩子函数异步并行全部执行。所有钩子的回调返回后,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
*/
3.6 AsyncSeriesHook
钩子函数异步串行全部执行,会保证钩子执行顺序,上一个钩子结束后,下一个才会开始。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
*/
3.7 AsyncParallelBailHook
钩子异步并行执行,即钩子都会执行,但只要有一个钩子返回了非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
*/
3.8 AsyncSeriesBailHook
钩子函数异步串行执行。但只要有一个钩子返回了非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
*/
3.9 AsyncSeriesWaterfallHook
钩子函数异步串行全部执行,上一个钩子返回的参数会传给下一个钩子。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
*/
3.10 AsyncSeriesLoopHook
钩子函数异步串行全部执行,某一步钩子函数会循环执行到返回非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
*/
3.11 HookMap
主要作用是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
*/
3.12 MultiHook
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:上一步钩子函数返回值就是下一步函数的参数。
- xxxLoopHook:钩子函数循环执行,直到返回值为undefined。
注意钩子函数返回值判断是和undefined对比,而不是和假值对比(null, false)
Hook也可以按同步、异步划分:
- syncXXX:同步钩子
- asyncXXX:异步钩子
Hook实例默认都有都有tap, tapAsync, tapPromise三个注册钩子回调的方法,不同注册方法生成的动态函数是不一样的。当然也并不是所有Hook都支持这几个方法,比如SyncHook不支持tapAsync, tapPromise。
Hook默认有call, callAsync,promise来执行回调。但并不是所有Hook都会有这几个方法,比如SyncHook不支持callAsync和promise。
四、实践应用
4.1 基于 tapable 实现类 jQuery.ajax()封装
我们先复习下jQuery.ajax()的常规用法(大概用法是这样,咱不纠结每个参数都正确):
jQuery.ajax({
url: 'api/request/url',
beforeSend: function(config) {
return config; // 返回false会取消此次请求发送
},
success: function(data) {
// 成功逻辑
}
error: function(err) {
// 失败逻辑
},
complete: function() {
// 成功,失败都会执行的逻辑
}
});
jQuery.ajax整个流程做了这么几件事:
- 在请求真正发送前,beforeSend提供了请求配置预处理的钩子。如果预处理函数返回false,能取消此次请求的发送。
- 请求成功(服务端数据返回后)执行success函数逻辑。
- 如果请求失败,则执行error函数逻辑。
- 最终,统一执行complete函数逻辑,无论请求成功还是失败。
同时,我们借鉴axios的做法,将beforeSend改为transformRequest,加入transformResponse,再加上统一的请求loading和默认的错误处理,这时我们整个ajax流程如下:
4.2 简单版的实现
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基石。
4.3 tapable在 Webpack中的应用
- Webpack中,一切皆插件(Hook)。
- Webpack通过tapable将这些插件串起来,组成固定流程。
- tapable解耦了流程任务和具体实现,同时提供了强大的扩展能力:拿到Hook,就能插入自己的逻辑。(我们平时写Webpack插件,就是找到对应的Hook去,然后注册我们自己的钩子函数。这样就方便地把我们自定义逻辑,插入到了Webpack任务流程中了)。
如果你需要强大的流程管理能力,可以考虑基于tapable去做架构设计。
五、小结
- tapable是一个流程管理工具。
- 提供了10种类型Hook,可以很方便地让我们去实现复杂的业务流程。
- tapable核心原理是基于配置,通过new Function方式,实时动态生成函数表达式去执行,从而完成逻辑
- tapable通过串联流程节点来实现流程控制,保证了流程的准确有序。
- 每个流程节点可以任意注册钩子函数,从而提供了强大的扩展能力。
- tapable是Webpack基石,它支撑了Webpack庞大的插件系统,又保证了这些插件的有序运行。
- 如果你也正在做一个复杂的流程系统(任务系统),可以考虑用tapable来管理你的流程。
作者:vivo-Ou Fujun
Webpack 基石 tapable 揭秘的更多相关文章
- Webpack的tapable 为什么要使用 new Funtion 来生成静态代码
为了保持代码的单态(monomorphism). 这涉及到了js引擎优化的一些问题, tapable从1.0.0版本开始就用new Function来生成静态代码最后来来执行, 以确保得到最优执行效率 ...
- 揭秘webpack plugin
前言 Plugin(插件) 是 webpack 生态的的一个关键部分.它为社区提供了一种强大的方法来扩展 webpack 和开发 webpack 的编译过程.这篇文章将尝试探索 webpack plu ...
- 关于webpack,打包时遇到的错误
最近在研究webpack这玩意,然后遇到一个问题,执行npm run build的时候,出现下面这个问题,各种搜索后,各种尝试,都没解决 运行时报错ERROR in ./src/app.vue Mod ...
- Tapable 0.2.8 入门
[原文:Tapable 0.2.8 入门] tapable是webpack的核心框架(4.0以上版本的API已经发生了变化),是一个基于事件流的框架,或者叫做发布订阅模式,或观察者模式,webpack ...
- 从0开始编写webpack插件
1. 前言 插件(plugins)是webpack中的一等功臣.正是由于有了诸多插件的存在,才使得webpack无所不能.在webpack源码中也是使用了大量的内部插件,插件要是用的好,可以让你的工作 ...
- 从 Tapable 中得到的启发
Tapable Why Tapable 前端开发中 Webpack 本质上是基于事件流的运行机制,它的工作流程是将特定的任务分发到指定的事件钩子中去完成.而实现这一切的核心就是 tapable,Web ...
- webpack打包原理
什么是 webpack ? 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler).当 webpack 处理应用程序时,它会递归地构建一个依 ...
- 初探webpack之编写plugin
初探webpack之编写plugin webpack通过plugin机制让其使用更加灵活,以适应各种应用场景,当然也大大增加了webpack的复杂性,在webpack运行的生命周期中会广播出许多事件, ...
- OBKoro1的2020年年终总结
前言 一晃眼2020年马上就要过去了,今年感觉过的特别快. 工作已经三年了,之前都没有写过年终总结,结果造成了下面这个现象: 回首过去的几年,记忆已经很模糊了,需要很用力才能想起过去一部分往事. 人生 ...
随机推荐
- Navicat 使用 SSH 通道
使用 Navicat for MySQL 通过跳板机登录 Mysql 时(使用 SSH 通道) 报错如下: SSH : Expected key exchange group packet from ...
- 牛客多校第九场H Cutting Bamboos(主席树 区间比k小的个数)题解
题意: 标记为\(1-n\)的竹子,\(q\)个询问,每次给出\(l,r,x,y\).要求为砍区间\(l,r\)的柱子,要求砍\(y\)次把所有竹子砍完,每次砍的时候选一个高度,把比他高的都砍下来,并 ...
- sdut3562-求字典序最小的最短路 按顶点排序后spfa的反例
首先我们可以这么搞...倒序建图,算出源点s附近的点距离终点的距离,然后判断一下,终点是否能跑到源点 能跑到的话呢,我们就判断s周围的点是否在最短路上,然后我们选编号最小的点就好了 代码 #inclu ...
- Asp.Net Core Grpc 入门实践
Grpc简介 gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架. 在 gRPC 中,客户端应用程序可以直接调用不同计算机上的服务器应用程序上的方法,就像它是本地对象一样,从而更轻松地创 ...
- 【Alpaca】.Net版开源配置中心 - 技术选型 Vue 3.0
是否可以用 Vue 3.0 现有的Vue 2.* 不推荐,坐等Vue 3.0出迁移工具吧,手动改的话工作量还是不小的 新项目 考虑下团队内对Vue + TS + VS Code的熟练程度.过程中你会遇 ...
- Linux Bash Script loop
Linux Bash Script loop shell 编程之流程控制 for 循环.while 循环和 until 循环 for var in item1 item2 ... itemN do c ...
- TS & ES-Next & playground
TS & ES-Next & playground TS TypeScript: TS Playground - An online editor for exploring Type ...
- node os env reader
node os env reader node-os-env-reader.js #!/usr/bin/env node "use strict"; /** * * @author ...
- URLSearchParams & Location & URL params parse
URLSearchParams & Location & URL params parse URL params parse node.js env bug node.js & ...
- HTTPS Proxy all in one
HTTPS Proxy all in one HTTP Proxy Charles Proxy https://www.charlesproxy.com/ Proxy SwitchyOmega 轻松快 ...