一文介绍 Tapable 的特性和使用,助力理解 webpack 插件架构!

一、引言

众所周知,webpack 的 plugins 非常灵活,可以在编译的不同阶段注册事件回调,这个功能便是基于 Tapable 实现的。

Tapable 的使用步骤如下:

  1. 创建钩子实例,如 SyncHookSyncLoopHook 钩子;
  2. 调用订阅接口注册事件回调,包括 taptapAsynctapPromise;
  3. 触发回调,包括 callcallAsyncpromise

Tapable 的钩子分为同步钩子和异步钩子,同步钩子都以 Sync 开头,通过 tap 注册回调,使用 call 触发回调。异步钩子则以 Async 开头,通过 tapAsynccallAsync,或者 tapPromisepromise 来注册和触发回调。

接下来便是各个钩子的介绍和用法。

二、同步钩子

SyncHook

SyncHook 是一个基础的钩子。通过该钩子注册的事件会依次执行。

const { SyncHook } = require('tapable')// 实例化钩子函数,可以在这里定义形参
const syncHook = new SyncHook(['author', 'age'])syncHook.intercept({// 每次调用 hook 实例的 tap() 方法注册回调函数时, 都会调用该方法,// 并且接受 tap 作为参数, 还可以对 tap 进行修改;register: (tapInfo) => {console.log(`${tapInfo.name} is doing its job`)return tapInfo // may return a new tapInfo object},// 通过hook实例对象上的call方法时候触发拦截器call: (arg1, arg2, arg3) => {console.log('Starting to calculate routes')},// 在调用被注册的每一个事件回调之前执行tap: (tap) => {console.log(tap, 'tap')},// loop类型钩子中 每个事件回调被调用前触发该拦截器方法loop: (...args) => {console.log(args, 'loop')},
})// 注册事件
syncHook.tap('监听器1', (name, age) => {console.log('监听器1:', name, age)
})syncHook.tap('监听器2', (name) => {console.log('监听器2:', name)
})syncHook.tap('监听器3', (name, age) => {console.log('监听器3:', name, age)
})// 触发事件,这里传的是实参,会被每一个注册的函数接收到
syncHook.call('wade', '25')// 执行结果
/**
监听器1 is doing its job
监听器2 is doing its job
监听器3 is doing its job
Starting to calculate routes
{ type: 'sync', fn: [Function (anonymous)], name: '监听器1' } tap
监听器1: wade 25
{ type: 'sync', fn: [Function (anonymous)], name: '监听器2' } tap
监听器2: wade
{ type: 'sync', fn: [Function (anonymous)], name: '监听器3' } tap
监听器3: wade 25
*/

SyncBailHook

SyncBailHook 是一个具有熔断风格的钩子。只要其中一个事件回调有返回值,后面的事件回调就不执行了。

const { SyncBailHook } = require('tapable')const hook = new SyncBailHook(['author', 'age'])hook.tap('测试1', (name, age) => {console.log('测试1接收参数:', name, age)
})hook.tap('测试2', (name, age) => {console.log('测试2接收参数:', name, age)return 'outer'
})hook.tap('测试3', (name, age) => {console.log('测试3接收参数:', name, age)
})hook.call('wade', '25')// 执行结果
/**
测试1接收参数: wade 25
测试2接收参数: wade 25
*/

第二个事件返回了 outer,因此第三个事件不会触发。

SyncLoopHook

SyncLoopHook 是一个循环类型的钩子。循环类型的含义是不停的循环执行事件回调,直到所有函数结果 result === undefined,不符合条件就返回从第一个事件回调开始执行。

const { SyncLoopHook } = require('tapable')const hook = new SyncLoopHook([])let count = 5hook.tap('测试1', () => {console.log('测试1里面的count:', count)if ([1, 2, 3].includes(count)) {return undefined} else {count--return '123'}
})hook.tap('测试2', () => {console.log('测试2里面的count:', count)if ([1, 2].includes(count)) {return undefined} else {count--return '123'}
})hook.tap('测试3', () => {console.log('测试3里面的count:', count)if ([1].includes(count)) {return undefined} else {count--return '123'}
})//通过call方法触发事件
hook.call()// 执行结果
/**
测试1里面的count: 5
测试1里面的count: 4
测试1里面的count: 3
测试2里面的count: 3测试1里面的count: 2
测试2里面的count: 2
测试3里面的count: 2测试1里面的count: 1
测试2里面的count: 1
测试3里面的count: 1
*/

SyncWaterfallHook

SyncWaterfallHook 是一个瀑布式类型的钩子。瀑布类型的钩子就是如果前一个事件回调的结果 result !== undefined,则 result 会作为后一个事件回调的第一个参数(也就是上一个函数的执行结果会成为下一个函数的参数)。

const { SyncWaterfallHook } = require('tapable')const hook = new SyncWaterfallHook(['author', 'age'])hook.tap('测试1', (name, age) => {console.log('测试1接收参数:', name, age)
})hook.tap('测试2', (name, age) => {console.log('测试2接收参数:', name, age)return '另外的名字'
})hook.tap('测试3', (name, age) => {console.log('测试3接收参数:', name, age)
})hook.call('wade', '25')// 执行结果
/**
测试1接收参数: wade 25
测试2接收参数: wade 25
测试3接收参数: 另外的名字 25
*/

三、异步钩子

异步钩子需要通过 tapAsync 函数注册事件,同时也会多一个 callback 参数,执行 callback 告诉 该注册事件已经执行完成。callback 函数可以传递两个参数** err** 和 result,一旦 err 不为空,则后续的事件回调都不再执行。异步钩子触发事件需要使用 callAsync

AsyncSeriesHook

AsyncSeriesHook 是一个串行的钩子。

const { AsyncSeriesHook } = require('tapable')const hook = new AsyncSeriesHook(['author', 'age']) // 先实例化,并定义回调函数的形参console.time('time')hook.tapAsync('测试1', (param1, param2, callback) => {console.log('测试1接收的参数:', param1, param2)setTimeout(() => {callback()}, 1000)
})hook.tapAsync('测试2', (param1, param2, callback) => {console.log('测试2接收的参数:', param1, param2)setTimeout(() => {callback()}, 2000)
})hook.tapAsync('测试3', (param1, param2, callback) => {console.log('测试3接收的参数:', param1, param2)setTimeout(() => {callback()}, 3000)
})hook.callAsync('wade', '25', (err, result) => {// 等全部都完成了才会走到这里来console.log('这是成功后的回调', err, result)console.timeEnd('time')
})// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: wade 25
测试3接收的参数: wade 25
这是成功后的回调 undefined undefined
time: 6.032
*/

AsyncSeriesBailHook

AsyncSeriesBailHook 是一个串行、熔断类型的钩子。一旦 callback 函数传递了参数,则不再执行后续的事件回调。

const { AsyncSeriesBailHook } = require('tapable')const hook = new AsyncSeriesBailHook(['author', 'age'])console.time('time')hook.tapAsync('测试1', (param1, param2, callback) => {console.log('测试1接收的参数:', param1, param2)setTimeout(() => {callback()}, 1000)
})hook.tapAsync('测试2', (param1, param2, callback) => {console.log('测试2接收的参数:', param1, param2)setTimeout(() => {callback(false, '123')}, 2000)
})hook.tapAsync('测试3', (param1, param2, callback) => {console.log('测试3接收的参数:', param1, param2)setTimeout(() => {callback()}, 3000)
})hook.callAsync('wade', '25', (err, result) => {// 等全部都完成了才会走到这里来console.log('这是成功后的回调', err, result)console.timeEnd('time')
})// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: wade 25
这是成功后的回调 null 123
time: 3.013s
*/

AsyncSeriesWaterfallHook

AsyncSeriesWaterfallHook 是一个串行、瀑布式类型的钩子。如果前一个事件回调的 callback 的参数 result !== undefined,则 result 会作为后一个事件回调的第一个参数(也就是上一个函数的执行结果会成为下一个函数的参数)。

const { AsyncSeriesHook } = require('tapable')const hook = new AsyncSeriesHook(['author', 'age'])console.time('time')hook.tapAsync('测试1', (param1, param2, callback) => {console.log('测试1接收的参数:', param1, param2)setTimeout(() => {callback()}, 1000)
})hook.tapAsync('测试2', (param1, param2, callback) => {console.log('测试2接收的参数:', param1, param2)setTimeout(() => {callback(null, 2441)}, 2000)
})hook.tapAsync('测试3', (param1, param2, callback) => {console.log('测试3接收的参数:', param1, param2)setTimeout(() => {callback()}, 3000)
})hook.callAsync('wade', '25', (err, result) => {// 等全部都完成了才会走到这里来console.log('这是成功后的回调', err, result)console.timeEnd('time')
})// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: 新名字A 25
测试3接收的参数: 新名字A 25
这是成功后的回调 null 新名字C
time: 6.043s
*/

AsyncParallelHook

AsyncParallelHook 是一个并行的钩子。通过这个钩子注册的回调不需要等待上一个事件回调执行结束便可以执行下一个事件回调。

const { AsyncParallelHook } = require('tapable')const hook = new AsyncParallelHook(['author', 'age'])console.time('time')hook.tapAsync('测试1', (param1, param2, callback) => {setTimeout(() => {console.log('测试1接收的参数:', param1, param2)callback()}, 2000)
})hook.tapAsync('测试2', (param1, param2, callback) => {console.log('测试2接收的参数:', param1, param2)callback()
})hook.tapAsync('测试3', (param1, param2, callback) => {console.log('测试3接收的参数:', param1, param2)callback()
})// call 方法只有同步钩子才有,异步钩子得使用 callAsync
hook.callAsync('wade', '25', (err, result) => {// 等全部都完成了才会走到这里来console.log('这是成功后的回调', err, result)console.timeEnd('time')
})// 执行结果
/**
测试2接收的参数: wade 25
测试3接收的参数: wade 25
测试1接收的参数: wade 25
这是成功后的回调 undefined undefined
time: 2.015s
*/

第一个事件设置了 2 秒的定时器再执行,此时会先执行后续的事件回调,等到 2 秒后再执行第一个事件回调。

AsyncParallelBailHook

AsyncParallelBailHook 是一个串行、熔断类型的钩子。若当前的事件回调的 callback 的参数 result !== undefined,则后续的事件都不再执行。

const { AsyncParallelBailHook } = require('tapable')const hook = new AsyncParallelBailHook(['author', 'age'])console.time('time')hook.tapAsync('测试1', (param1, param2, callback) => {console.log('测试1接收的参数:', param1, param2)setTimeout(() => {callback()}, 1000)
})hook.tapAsync('测试2', (param1, param2, callback) => {console.log('测试2接收的参数:', param1, param2)callback(null, '测试2有返回值啦')
})hook.tapAsync('测试3', (param1, param2, callback) => {console.log('测试3接收的参数:', param1, param2)setTimeout(() => {callback(null, '测试3有返回值啦')}, 3000)
})hook.callAsync('wade', '25', (err, result) => {// 等全部都完成了才会走到这里来console.log('这是成功后的回调', err, result)console.timeEnd('time')
})// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: wade 25
这是成功后的回调 null 测试2有返回值啦
time: 3.015s
*/

由于第二个事件回调有返回值,因此第三个事件回调不会执行。若第一个事件回调也有返回值,尽管是第二个事件回调先执行完成,但是 callAsync 拿到的结果依然是第一个事件回调的返回值,示例如下:

const { AsyncParallelBailHook } = require('tapable')const hook = new AsyncParallelBailHook(['author', 'age'])console.time('time')hook.tapAsync('测试1', (param1, param2, callback) => {console.log('测试1接收的参数:', param1, param2)setTimeout(() => {callback(null, '测试1有返回值啦')}, 3000)
})hook.tapAsync('测试2', (param1, param2, callback) => {console.log('测试2接收的参数:', param1, param2)callback(null, '测试2有返回值啦')
})hook.tapAsync('测试3', (param1, param2, callback) => {console.log('测试3接收的参数:', param1, param2)setTimeout(() => {callback(null, '测试3有返回值啦')}, 1000)
})hook.callAsync('wade', '25', (err, result) => {// 等全部都完成了才会走到这里来console.log('这是成功后的回调', err, result)console.timeEnd('time')
})// 执行结果
/**
测试1接收的参数: wade 25
测试2接收的参数: wade 25
这是成功后的回调 null 测试1有返回值啦
time: 3.009s
*/

四、总结

Tapable 提供的这些钩子,支持同步、异步、熔断、循环、waterfall 等功能特性,以此支撑起 webpack 复杂的编译功能,在理解这些内容之后,我们对 webpack 插件架构的设计会有进一步的理解和使用。

参考资料

https://juejin.cn/post/6955421936373465118
https://github.com/webpack/tapable

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/35227.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【树状数组 队列】1505. 最多 K 次交换相邻数位后得到的最小整数

本文涉及知识点 树状数组 队列 LeetCode1505. 最多 K 次交换相邻数位后得到的最小整数 给你一个字符串 num 和一个整数 k 。其中,num 表示一个很大的整数,字符串中的每个字符依次对应整数上的各个 数位 。 你可以交换这个整数相邻数位的数字 最多 k 次…

在线epub阅读器epub;在线图书阅读器;专门为epub定制的阅读器;免费在线电子图书epub阅读器

背景:不记得某时某刻了,就是当时想要使用电脑阅读epub图书,也找了好些个在线epub阅读器,但总有一些不如意的地方,如某些功能需要会员之类的,突发临想的就想到自己开发一个,就此,一个…

Python笔记 文件的写,追加,备份操作

一、文件的写操作 案例演示: # 1.打开文件 f open(python.txt,w)# 2.文件写入 f.write(hello world)# 3.内容刷新 f.flush() 注意: 直接调用write,内容并为真正的写入文件,二十会积攒在程序的内存中,称之为缓冲区…

Android SurfaceFlinger——OpenGL ES初始化(十三)

上一篇文章我们对 OpenGL ES 相关知识有了一定的了解,并知道在使用 OpenGL ES 是需要先通过 eglGetDisplay() 方法获取 EGLDisplay 默认主屏幕句柄。这里就分析一下 eglGetDisplay() 中的 egl_init_drivers() 初始化 OpenGL ES 的对应流程。 一、OpenGL ES初始化 1、egl.cpp…

【Docker】Docker网络模式

1、概述 docker run创建Docker容器时,可以用–net选项指定容器的网络模式,Docker有以下4种网络模式:bridge模式:使--net bridge指定,默认设置;host模式:使--net host指定;none模式&…

前端技术栈学习:Vue2、Vue cli脚手架、ElementUI组件库、Axios

1 基本介绍 (1)Vue 是一个前端框架, 易于构建用户界面 (2)Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或项目整合 (3)支持和其它类库结合使用 (4&#…

期末复习题中的问题

一、编程中(包括函数)的问题 1. malloc 头文件是stdlib.h 二、第二次写复习题的不会的 三、程序填空 总结: 删除节点m >>>>要有一个指针来遍历找到这个m >>>> 用另一个指针指向这个指针的下一 个 >>&…

达梦(DM8)数据库备份与还原(逻辑备份)一

一、达梦数据库的逻辑备份分四种级别的导出(dexp)与导入(dimp)的备份 第一种是:数据库级:导出或导入数据库中所有的对象。主要参数是:FULL 第二种是:用户级别:导出或导…

小程序的生命周期使用方法和应用场景

小程序生命周期 初始化(App Launch) • 触发时机:小程序首次启动时。 • 主要事件:onLaunch。 • 功能与适用场景: • 全局数据初始化:设置应用的全局状态和变量。 • 登录状态检查:判断用户是…

FastAPI 表单数据

FastAPI 表单数据 FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,与 Python 3.6+ 类型提示一起使用。它是一个轻量级的框架,但功能强大,能够处理各种类型的请求数据,包括 JSON、表单数据和文件等。在本文中,我们将重点讨论如何在 FastAPI 中处理表单数据…

vue+go实现web端连接Linux终端

vuego实现web端连接Linux终端 实现效果 实现逻辑1——vue 依赖包 "xterm": "^5.3.0","xterm-addon-attach": "^0.9.0","xterm-addon-fit": "^0.8.0"样式和代码逻辑 <template><a-modalv-model:visib…

FileNotFoundError: Cannot find DGL C++ graphbolt library at ...

FileNotFoundError: Cannot find DGL C graphbolt library at ...-CSDN博客https://blog.csdn.net/weixin_44017989/article/details/137658749

k8s手撕架构图+详解

“如果您在解决类似问题时也遇到了困难&#xff0c;希望我的经验分享对您有所帮助。如果您有任何疑问或者想分享您的经历&#xff0c;欢迎在评论区留言&#xff0c;我们可以一起探讨解决方案。祝您在编程路上顺利前行&#xff0c;不断突破技术的难关&#xff0c;感谢您的阅读&a…

【LeetCode】一、数组相关(双指针算法 + 置换)

文章目录 1、算法复杂度1.1 时间复杂度1.2 空间复杂度 2、数组3、leetcode485&#xff1a;最大连续1的个数4、leetcode283&#xff1a;移动05、leetcode27&#xff1a;移除元素 1、算法复杂度 1.1 时间复杂度 算法的执行时间与输入值之间的关系&#xff08;看代码实际总行数的…

Angular 指令

Angular 指令是 Angular 框架中的一项核心功能&#xff0c;它允许开发人员扩展 HTML 的功能&#xff0c;并创建可复用的组件和行为。以下是一些常见的 Angular 指令&#xff1a; 1. 组件指令 (Component Directives) 组件指令是最常用的一种指令&#xff0c;用于创建可复用的 U…

NineData和华为云在一起!提供一站式智能数据库DevOps平台

以GuassDB数据库为底座 NineData和华为云一起 为企业提供 一站式智能数据库DevOps平台 帮助开发者 高效、安全地完成 数据库SQL审核 访问控制、敏感数据保护等 日常数据库相关开发任务 NineData 智能数据管理平台 NineData 作为新一代的云原生智能数据管理平台&#xf…

const data = this.info为什么修改data时this.info也跟着变?

const data this.info 这种情况是data和this.info指向了同一个对象&#xff0c;只是将 this.info 的引用赋值给了 data &#xff08;可以理解为指向同一个地址&#xff09;也就是说如果修改 data 对象的属性或内容&#xff0c;那么 this.info 也会反映出这些变化。 原始类型&…

Manjaro Linux系统简介和archlinux哲学

## Manjaro Linux系统简介 Manjaro Linux是一个基于Arch Linux的操作系统&#xff0c;以其用户友好性和滚动更新机制而受到广泛欢迎。它为用户提供了一个易于安装和使用的平台&#xff0c;同时保持了Linux系统的高度定制性和最新的软件特性。 ### Manjaro Linux与Arch Linux的…

HSRP热备份路由协议(VRRP虚拟路由冗余协议)配置以及实现负载均衡

1、相关原理 在网络中&#xff0c;如果一台作为默认网关的三层交换机或者路由器损坏&#xff0c;所有使用该网关为下一跳的主机通信必然中断&#xff0c;即使配置多个默认网关&#xff0c;在不重启终端的情况下&#xff0c;也不能彻底换到新网关。Cisco提出了HSRP热备份路由协…

golang学习笔记——接口经典面试题 value receivers与pointer receiver

下面的代码是一个比较好的面试题 请问下面的代码是否能通过编译&#xff1f; package mainimport "fmt"type People interface {Speak(string) string }type Student struct{}func (stu *Student) Speak(think string) (talk string) {if think "sb" {t…