写点什么

Webpack 的插件机制 - Tapable

  • 2021-07-31
  • 本文字数:3719 字

    阅读完需:约 12 分钟

Webpack 的插件机制 - Tapable

前言


用了这么久的 Webpack,你一定对它的生态重要组成部分loaderplugin很好奇吧,你是否尝试过编写自己的插件呢,是否了解过 Webpack 的插件机制呢,什么?没有,那还不赶紧上车学一波!


1、tapable


Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。Webpack 通过 Tapable 来组织这条复杂的生产线。Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。——「深入浅出 Webpack」


作为 Webpack 的核心库,tabpable承包了 Webpack 最重要的事件工作机制,包括 Webpack 源码中高频的两大对象(compilercompilation)都是继承自Tapable类的对象,这些对象都拥有Tapable的注册和调用插件的功能,并向外暴露出各自的执行顺序以及hook类型,详情可见文档


2、tapable 的钩子


const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
复制代码


上面是官方文档给出的 9 种钩子的类型,我们看命名就能大致推测他们的类型和区别,分成同步、异步,瀑布流、串行、并行类型、循环类型等等,钩子的目的是为了显式地声明,触发监听事件时(call)传入的参数,以及订阅该钩子的 callback 函数所接受到的参数,举个最简单的🌰


const sync = new SyncHook(['arg']) // 'arg' 为参数占位符sync.tap('Test', (arg1, arg2) => {  console.log(arg1, arg2) // a,undefined})sync.call('a', '2')
复制代码


上述代码定义了一个同步串行钩子,并声明了接收的参数的个数,可以通过hook实例对象(SyncHook本身也是继承自Hook类的)的tap方法订阅事件,然后利用call函数触发订阅事件,执行 callback 函数,值得注意的是 call 传入参数的数量需要与实例化时传递给钩子类构造函数的数组长度保持一致,否则,即使传入了多个,也只能接收到实例化时定义的参数个数。


序号钩子名称执行方式使用要点
1SyncHook同步串行不关心监听函数的返回值
2SyncBailHook同步串行只要监听函数中有一个函数的返回值不为 null,则跳过剩余逻辑
3SyncWaterfallHook同步串行上一个监听函数的返回值将作为参数传递给下一个监听函数
4SyncLoopHook同步串行当监听函数被触发的时候,如果该监听函数返回 true 时则这个监听函数会反复执行,如果返回 undefined 则表示退出循环
5AsyncParallelHook异步并行不关心监听函数的返回值
6AsyncParallelBailHook异步并行只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到 callAsync 等触发函数绑定的回调函数,然后执行这个被绑定的回调函数
7AsyncSeriesHook异步串行不关心 callback()的参数
8AsyncSeriesBailHook异步串行callback()的参数不为 null,就会直接执行 callAsync 等触发函数绑定的回调函数
9AsyncSeriesWaterfallHook异步串行上一个监听函数的中的 callback(err, data)的第二个参数,可以作为下一个监听函数的参数

上述表格罗列了所有 hook 的使用方式和要点。


3、注册事件回调


注册事件回调有三个方法:taptapAsync 和 tapPromise,其中 tapAsync 和 tapPromise 不能用于 Sync 开头的钩子类,强行使用会报错。tap的使用方式在上文已经展示过了,就用官方文档的例子展示下tapAsync的使用方式,相比于taptapAsync需要执行 callback 函数才能确保流程会走到下一个插件中去。


myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin", (source, target, routesList, callback) => { bing.findRoute(source, target, (err, route) => {  if(err) return callback(err);  routesList.add(route);  // call the callback  callback(); });});
复制代码


4、触发事件


触发事件的三个方法是与注册事件回调的方法一一对应的,这点从方法的名字上也能看出来:call 对应 tapcallAsync 对应 tapAsync 和 promise 对应 tapPromise。一般来说,我们注册事件回调时用了什么方法,触发时最好也使用对应的方法。同样需要注意的是 callAsync 有个 callback 函数,在逻辑完毕时需要执行,一些具体用法类似于上面的注册事件类似,就不一一展开了。


5、了解机制


那么在 Webpack 中到底如何使用 tapable 调用这些 plugin 呢?


我们首先来看官网给出的编写一个 plugin 的示例


class HelloWorldPlugin {  apply(compiler) {    compiler.hooks.done.tap('Hello World Plugin', (      compilation /* compilation is passed as an argument when done hook is tapped.  */    ) => {      console.log('Hello World!');    });  }}
module.exports = HelloWorldPlugin;
复制代码


上述代码块编写了一个叫 HelloWorldPlugin 的类,它提供了一个叫apply的方法,在该方法中我们可以从外部获取到 Webpack 执行全过程中单一的compiler实例,通过compiler实例,我们可以在 Webpack 的生命周期的done节点(也就是上面我们提到的hook)tap 一个监听事件,也就是说当 Webpack 全部流程执行完毕时,监听事件将会被触发,同时stat统计信息会被传入到监听事件中,在事件中,我们就可以通过stat做一系列我们想要做的数据分析。一般来说,使用一个 Webpack 插件,需要在 Webpack 配置文件中导入(import)插件的类,new 一个实例,like this:


// Webpack.config.jsvar HelloWorldPlugin = require('hello-world');
module.exports = { // ... configuration settings here ... plugins: [new HelloWorldPlugin({ options: true })]};
复制代码


这里聪明的你一定想到了 Webpack 应该是读取了这份配置文件后获得了HelloWorldPlugin实例,并调用了实例的apply方法,在done节点上添加了监听事件!没错,让我们来追溯下 Webpack 的源码部分,在 Webpack 项目的lib/Webpack.js文件中,我们可以看到


if (options.plugins && Array.isArray(options.plugins)) {    for (const plugin of options.plugins) {  if (typeof plugin === "function") {   plugin.call(compiler, compiler);  } else {   plugin.apply(compiler);  } }}
复制代码


这段代码中options就是指配置文件导出的整个对象,这里可以看到 Webpack 循环遍历了一遍 plugins,并分别调用了他们的 apply 方法,当然如果 plugin 是function类型,就直接用call来执行,这也就是我上文提到的一般来说的例外,如果你的插件逻辑很简单,你可以直接在配置文件里写一个function,去执行你的逻辑,而不必啰嗦的写一个类或者用更纯粹的prototype去定义类的方法。到这里为止,我们已经了解了插件中的监听事件是如何注册到 Webpack 的compilecompilationtapable类)上去的,那监听事件是如何、何时被触发的呢,理论上应该是先注册完毕,后触发,这样监听事件才有意义,我们接着发现,在lib/Compiler.js中的Compiler类的run函数里有这样一段代码


const onCompiled = (err, compilation) => { if (err) return finalCallback(err);
if (this.hooks.shouldEmit.call(compilation) === false) { ... this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); return; }
this.emitAssets(compilation, err => { if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) { ... this.hooks.done.callAsync(stats, err => { ... }); return; }
this.emitRecords(err => { if (err) return finalCallback(err);
... this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); }); });};... this.compile(onCompiled);
复制代码


回调函数onCompiled会在compile过程结束时被调用,无论走到哪个 if 逻辑中,this.hooks.done.callAsync都会被执行,也就是说在 done 节点上注册的监听事件会按照顺序依次被触发执行。接着我们再向上追溯,包裹了onCompiled函数的run函数是在lib/Webpack.js中被执行的


if (Array.isArray(options)) {    ...} else if (typeof options === "object") {    ... compiler = new Compiler(options.context); compiler.options = options; if (options.plugins && Array.isArray(options.plugins)) {  for (const plugin of options.plugins) {   if (typeof plugin === "function") {    plugin.call(compiler, compiler);   } else {    plugin.apply(compiler);   }  } }} else { ...}if (callback) { ... compiler.run(callback);}
复制代码


刚好在plugin.apply()的后面,所以是符合先注册监听事件,再触发的逻辑顺序的。


是不是已经有点乱了,来来来,我们用流程图简单捋一下。

插件的注册执行流程图示



6、总结


tapable 作为 Webpack 的核心库,承接了 Webpack 最重要的事件流的运转,它巧妙的钩子设计很好的将实现与流程解耦开来,真正实现了插拔式的功能模块,在 Webpack 中最核心的负责编译的 Compiler 和负责创建的 bundles 的 Compilation 都是 Tapable 的实例,可以说想要真正读懂 Webpack,tapable 的知识储备是必不可少的,它的一些设计思想也是很值得我们借鉴的,本文只是对 tapable 的一些 api 以及 Webpack 如何使用 tapable 串起了整个插件流工作机制做了介绍。



头图:Unsplash

作者:丁楠

原文:https://mp.weixin.qq.com/s/qWq46-7EJb0Byo1H3SDHCg

原文:Webpack 的插件机制 - Tapable

来源:微医大前端技术 - 微信公众号 [ID:wed_fed]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-07-31 22:004217

评论

发布
暂无评论
发现更多内容

《后浪》产品经理篇(恶搞版)

静陌

产品经理 后浪

Redis稳定性实践

心平气和

redis 缓存 稳定性

尽管HTTP/3已经来了,HTTP/2也得了解啊

清远

网络协议 HTTP

设计模式之观察者模式

设计模式

一文带你看清HTTP所有概念

苹果看辽宁体育

HTTP

谈谈控制感(5):怎么破控制感损失的局

史方远

职场 心理 成长

更聪明地学习,而不是苦读——《如何高效学习》

mzlogin

学习

Mac 使用笔记

FeiLong

聊聊我对技术一些性质的认识

Tanzv

技术 思考 新人

让你高效工作与学习的免费工具(1)

石云升

高效工作 效率工具 工具

严选合伙人(二)

Neco.W

创业 重新理解创业 合伙人

业余前端的日常

顿晓

学习 大前端 日常 专家 知识体系

回“疫”录(18):536公里的路

小天同学

疫情 回忆录 现实纪录 纪实 返程

Xtrabackup的安装使用

一个有志气的DB

MySQL 工具 数据的分片和备份

对于程序员,那些既陌生又熟悉的计算机硬件

架构师修行之路

微软 编程 程序员 cpu 架构师

Python 核心技术与进阶 list & tuple

Bonaparte

Java新技术:封闭类

X.F

Java 架构 编程语言

面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)

一枝花算不算浪漫

源码 并发编程 ThreadLocal

Android | Tangram动态页面之路(二)介绍

哈利迪

android

从一次排查ES线上问题得出的总结——熔断机制

罗琦

elasticsearch 源码分析 circuit break 熔断

如何在一台计算机上安装多个 JDK 版本

mghio

Java jdk 版本管理工具

Harbor 2.0的飞跃: OCI 兼容的工件仓库

亨利笔记

Kubernetes 容器 k8s Harbor 镜像

《零基础学 Java》 FAQ 之 7-Java 中的内存是怎么分配的

臧萌

Java JVM

Rust 与区块链四月月刊

Aimee 阿敏

区块链 rust 加密货币 crypto

MySQL常用权限说明

一个有志气的DB

MySQL 用户研究

游戏夜读 | Scikit-learn迎来0.21之前

game1night

如果你觉得学习 Git 很枯燥,那是因为你还没玩过这款游戏!

GitHubDaily

git GitHub 编程 程序员 开发者工具

OpenResty部署配置和日志切割

wong

nginx centos openresty

sync.Map源码分析

陈思敏捷

源码 源码分析 Go 语言

松哥手把手带你入门 Spring Security,别再问密码怎么解密了

江南一点雨

Java spring Spring Boot spring security

松哥手把手教你定制 Spring Security 中的表单登录

江南一点雨

Java spring Spring Boot spring security

Webpack 的插件机制 - Tapable_大前端_微医大前端技术_InfoQ精选文章