「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
前言
webpack plugin
是webpack的重要组成部分,webpack的功能有80%以上都是使用plugin实现的,本文分上中下三篇,从了解tapable
去了解webpack plugin
的运作机制,以及webpack plugin的api介绍,以及plugin实践。分别内容如下:
- 基础
- webpack plugin 介绍
- tapable 模块介绍
- 重点
- compiler 介绍
- compilation 介绍
- 扩展
- 其他hook API 介绍
- plugin 实践
webpack plugin
在详细了解webpack插件之前,需要先了解webpack的一些基本工作原理。
依赖分析
webpack主要功能是从入口文件开始读取文件源码,找到它的依赖。然后读取依赖文件,继续找它们的依赖,一直递归下去。
模块映射
在依赖分析期间,webpack会进行模块映射,把分析过的文件内容都放在一个map中(大文件),通过文件路径及文件名相关联的值作为key。
API介绍
在上述过程中,webpack提供了一系列hook及相关对象,让开发者可以在webpack打包的过程中做一些优化处理,或做一些额外的自定义处理。
- compiler: 顶级API,提供大部分webpack执行钩子。提供webpack执行参数对象以及配置信息对象。
- compilation: 通过compiler访问,能获取到编译时的模块信息,依赖信息
- Resolvers: webpack会把entry的配置路径提供给 Resolver,它会检查给定的部分路径是否存在,并返回完整的绝对路径以及上下文、请求、调用等额外信息。在
compiler
类中,提供了三种类型的内置解析器:normal
: 通过绝对或相对路径解析模块。context
: 在给定的上下文中解析模块。loader
: 解析 webpack loader。
- NormalModuleFactory 与 ContextModuleFactory Hooks: 工厂创建对象或实例。从入口点开始,它解析每个请求,解析内容以查找进一步的请求,并通过解析所有文件和解析任何新文件来继续爬取文件。在最后阶段,每个依赖项都成为一个 Module 实例。
- JavascriptParser: 提供模块解释的API,让开发者可以自定义处理模块解释的过程。
Tapable
上文介绍的webpack API都继承了Tapable类。Tapable使用发布订阅模式,封装了一系列API控制钩子函数的发布与订阅。
接下来主要介绍tapable模块下4个类:
- SyncHook: 同步钩子,任务会从先到后依次逐个执行。
- SyncBailHook: 确保同步钩子,提供终止机制中断钩子任务执行。
- AsyncSeriesHook: 异步串行任务钩子。
- AsyncParallelHook: 异步并行任务钩子。
准备工作
创建一个 tapable_test 目录,进入目录,创建 test.js。
|- tapable_test
|- test.js
安装 tapable 依赖。
npm i -D tapable
//或
yarn add -D tapable
SyncHook
同步执行钩子,触发所有消费者,消费者回调依次执行。
test.js内容如下:
const { SyncHook } = require('tapable');
class TapableTest {
constructor() {
this.hooks = {
// 实例化同步钩子,赋值给 hooks.syncHook, 接收两个参数。
syncHook: new SyncHook(['params1', 'params2'])
}
}
// 添加消息消费者
init() {
// hooks.syncHook 为 SyncHook类的实例, 使用 tap 方法添加消息的消费者
this.hooks.syncHook.tap('pluginA', (params1, params2) => {
console.log(new Date(), 'pluginA', params1, params2)
return true
})
this.hooks.syncHook.tap('pluginB', (params1, params2) => {
console.log(new Date(), 'pluginB', params1, params2)
return true
})
this.hooks.syncHook.tap('pluginC', (params1,params2) => {
console.log(new Date(), 'pluginC', params1, params2)
return true
})
return this
}
// 调用
start() {
// 触发syncHook,并传入参数
this.hooks.syncHook.call({test: 'hello world'}, 'SyncHook');
}
}
const test = new TapableTest()
test.init()
test.start()
控制台命令行执行:
node test.js
输出结果:
2022-02-24T14:50:15.255Z pluginA { test: 'hello world' } SyncHook
2022-02-24T14:50:15.262Z pluginB { test: 'hello world' } SyncHook
2022-02-24T14:50:15.262Z pluginC { test: 'hello world' } SyncHook
实例化SyncHook对象后,使用tap方法注册消费者,使用call方法触发所有消费者。从输出结果可以看出消费者按注册顺序依次执行。
SyncBailHook
确保同步钩子,按顺序执行注册的消费者回调,一旦其中一个消费者return返回值,立刻中断后续执行。
test.js内容更新如下:
const { SyncBailHook } = require('tapable');
class TapableTest {
constructor() {
this.hooks = {
// 实例化同步钩子,赋值给 hooks.syncBailHook, 接收两个参数。
syncBailHook: new SyncBailHook(['params1', 'params2'])
}
}
// 添加消息消费者
init() {
// hooks.syncBailHook 为 SyncBailHook类的实例, 使用 tap 方法添加消息的消费者
this.hooks.syncBailHook.tap('pluginA', (params1, params2) => {
console.log(new Date(), 'pluginA', params1, params2)
})
this.hooks.syncBailHook.tap('pluginB', (params1, params2) => {
console.log(new Date(), 'pluginB', params1, params2)
return true
})
this.hooks.syncBailHook.tap('pluginC', (params1,params2) => {
console.log(new Date(), 'pluginC', params1, params2)
})
return this
}
// 调用
start() {
// 触发syncBailHook,并传入参数
this.hooks.syncBailHook.call({test: 'hello world'}, 'syncBailHook');
}
}
const test = new TapableTest()
test.init()
test.start()
控制台命令行执行:
node test.js
输出结果:
2022-02-24T15:31:15.835Z pluginA { test: 'hello world' } syncBailHook
2022-02-24T15:31:15.843Z pluginB { test: 'hello world' } syncBailHook
实例化SyncBailHook对象后,使用tap方法注册消费者,使用call方法触发所有消费者。其中pluginB消费者回调函数中return true,中断后续执行。
AsyncSeriesHook
异步串行任务钩子,并行执行注册回调。和同步钩子不一样,需要使用tapAsync或tapPromise注册消费者,callAsync触发消费者。
const { AsyncSeriesHook } = require('tapable');
class TapableTest {
constructor() {
this.hooks = {
// 实例化同步钩子,赋值给 hooks.asyncSeriesHook, 接收两个参数。
asyncSeriesHook: new AsyncSeriesHook(['params1', 'params2'])
}
}
// 添加消息消费者
init() {
// hooks.asyncSeriesHook 为 AsyncSeriesHook类的实例, 使用 tapAsync / tapPromise方法添加消息的消费者
this.hooks.asyncSeriesHook.tapAsync('pluginA', (params1, params2, cb) => {
setTimeout(()=>{
console.log(new Date(), 'pluginA', params1, params2)
cb()
}, 1000)
})
this.hooks.asyncSeriesHook.tapAsync('pluginB', (params1, params2, cb) => {
setTimeout(()=>{
console.log(new Date(), 'pluginB', params1, params2)
cb()
}, 2000)
})
this.hooks.asyncSeriesHook.tapPromise('pluginC', (params1,params2) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(new Date(), 'pluginC', params1, params2)
resolve();
}, 3000)
})
})
return this
}
// 调用
start() {
// 触发asyncSeriesHook,并传入参数与回调
this.hooks.asyncSeriesHook.callAsync({test: 'hello world'}, 'asyncSeriesHook', () => {
console.log('all is done')
});
}
}
const test = new TapableTest()
test.init()
test.start()
控制台命令行执行:
node test.js
输出结果:
2022-02-24T15:53:59.401Z pluginA { test: 'hello world' } asyncSeriesHook
2022-02-24T15:54:01.410Z pluginB { test: 'hello world' } asyncSeriesHook
2022-02-24T15:54:04.411Z pluginC { test: 'hello world' } asyncSeriesHook
all is done
异步消费者注册有tapAsync和tapPromise两种方式:
- tapAsync 形参最后将会是告知调用这消费完成的回调函数,需要在自定义逻辑执行完成后执行该回调函数。
- tapPromise 返回一个Promise实例,需要在Promise实例构造函数入参回调中执行resolve告知调用这当前消费已完成。
异步消费者使用callAsync调用,可以传一个结束回调函数,在所有异步消费者都被执行后触发执行。控制台最后打印出all is done
。
由于是串行执行消费者,所以控制台日志输出的时间间隔分别是1000ms
、2000ms
、3000ms
。
AsyncParallelHook
异步并行任务钩子,并行执行注册回调。和同步钩子不一样,需要使用tapAsync或tapPromise注册消费者,callAsync触发消费者。
控制台命令行执行:
const { AsyncParallelHook } = require('tapable');
class TapableTest {
constructor() {
this.hooks = {
// 实例化同步钩子,赋值给 hooks.asyncParallelHook, 接收两个参数。
asyncParallelHook: new AsyncParallelHook(['params1', 'params2'])
}
}
// 添加消息消费者
init() {
// hooks.asyncParallelHook 为 AsyncParallelHook类的实例, 使用 tapAsync / tapPromise方法添加消息的消费者
this.hooks.asyncParallelHook.tapAsync('pluginA', (params1, params2, cb) => {
setTimeout(()=>{
console.log(new Date(), 'pluginA', params1, params2)
cb()
}, 1000)
})
this.hooks.asyncParallelHook.tapAsync('pluginB', (params1, params2, cb) => {
setTimeout(()=>{
console.log(new Date(), 'pluginB', params1, params2)
cb()
}, 2000)
})
this.hooks.asyncParallelHook.tapPromise('pluginC', (params1,params2) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log(new Date(), 'pluginC', params1, params2)
resolve();
}, 3000)
})
})
return this
}
// 调用
start() {
// 触发asyncParallelHook,并传入参数与回调
this.hooks.asyncParallelHook.callAsync({test: 'hello world'}, 'asyncParallelHook', () => {
console.log('all is done')
});
}
}
const test = new TapableTest()
test.init()
test.start()
输出结果:
2022-02-24T15:51:11.116Z pluginA { test: 'hello world' } asyncParallelHook
2022-02-24T15:51:12.116Z pluginB { test: 'hello world' } asyncParallelHook
2022-02-24T15:51:13.116Z pluginC { test: 'hello world' } asyncParallelHook
all is done
AsyncParallelHook
的API与AsyncSeriesHook
的API雷同,差别在于并行与串行:
由于是并行执行消费者,所以控制台日志输出的时间间隔分别是1000ms
、1000ms
、1000ms
。
总结
webpack plugin的主要Api都是继承tapable
库中的类实现的。通过了解SyncHook
、SyncBailHook
、AsyncParallelHook
、 AsyncSeriesHook
类对webpack plugin下的hook api,串行、并行、同步、异步操作有一个大概的了解。以便更好的阅读接下来的内容。
了解tapable
的确是webpack plugin
入门中的入门,谢谢大家阅读。