写点什么

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:004372

评论

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

Tapdata 在线研讨会:实时数据同步应用场景及实现方案探讨

tapdata

MySQL oracle mongodb

云行·数治·慧用丨边缘云一体机赋能基层实现边缘侧数据智能

云计算

谈谈PhxSQL的设计和实现哲学(上)

OpenIM

遭GitHub封杀!百万人竟跪求这份阿里内部Java面试手册

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

史上最强!这份在各大平台获百万推荐的Java核心手册实至名归

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

终于有阿里P8从开发、运维两个角度总结出了Redis实战手册

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

震撼!多名阿里资深专家联合撰写深入理解Redis设计源码手册

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

源码 | 解析 Redo Log 实现方式

RadonDB

MySQL 数据库 RadonDB

去中心化钱包开发,imtoken钱包开发

9月日更挑战正式开始啦~

InfoQ写作社区官方

9月日更 热门活动

五岳核心版上线!这份阿里开发手册核心版又将被多少人疯狂转载?

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

Pandas教程-3-DataFrame数据筛选(上)

Peter

Python 数据分析 pandas

Pandas教程-4-DataFrame数据筛选(中)

Peter

Python 数据分析 pandas

String split()方法踩坑

梁小航航

string

第一波场DAPP系统搭建|DAPP介绍

Geek_23f0c3

DAPP智能合约交易系统开发 波场链DAPP开发 第一波场

上线几小时下载量破百万!无价的这份阿里并发编程图册就这么强势

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

拒不外传!阿里内部耗重金找人总结出这份并发编程手册(全彩版)

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

Pandas教程-1-Series类型数据

Peter

Python 数据分析 pandas

源码大放送:基于Pyecharts的苏州旅游攻略

Peter

Python 数据分析 爬虫

Nebula Graph 源码解读系列 | Vol.00 序言

NebulaGraph

图数据库 源码解读 分布式图数据库

mycat入门:简介和安装

小鲍侃java

9月日更

我靠!都金三银四了还有人没看过阿里这份Java面试核心手册?

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

Pandas教程-2-10种方式创建DataFrame

Peter

Python 机器学习 pandas

自动化测试框架指南

FunTester

自动化测试 测试框架 数据驱动 FunTester 测试自动化

configparser 配置文件解析器

林十二XII

谈谈PhxSQL的设计和实现哲学(下)

OpenIM

合约量化策略系统搭建,合约策略交易软件开发

IOS技术分享| 在iOS WebRTC 中添加美颜滤镜

anyRTC开发者

音视频 WebRTC 视频通话 ios技术分享 美颜

☕【JVM技术指南】「难点-核心-遗漏」TLAB内存分配+锁的碰撞(技术串烧)!

码界西柚

JVM TLAB 锁升级 内存分配 9月日更

Tapdata肖贝贝:实时数据引擎系列(三) - 流处理引擎对比

tapdata

炸裂!阿里十年老兵总结出SpringCloud入门到实战手册

公众号_愿天堂没有BUG

Java 编程 程序员 架构 面试

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