Webpack 源码中有两个非常重要的类:Compiler 和 Compilation,它们通过注入插件的方式来监听 Webpack 所有的生命周期,插件的注入离不开各种各样的 Hook,它们中的 Hook 其实是创建了 Tapable 库中的各种 Hook 实例。
Tapable:
Tapable 是官方编写和维护的一个库,管理着需要的 Hook,这些 Hook 可以被应用到插件中。
Tapable 中的 Hook:
以 sync 开头的是同步的 Hook;以 async 开头的是异步的 Hook,也就是说,如果有两个事件处理回调,不会等待上一次处理回调结束后才执行下一次回调。
- 新建
src/index.js
文件,并编写代码。// 从 tapable 中导入需要的 hook 类 const {SyncHook} = require('tapable') class CustomPlugin { constructor() { this.hooks = { // 通过 new SyncHook() 来实例化一个 hook,赋值给自定义的 hook。接受一个字符串数组作为参数,数组的值可以任意写,重要的是个数需要大于等于调用自定义 hook 注册的事件时传入的参数的个数 syncHook: new SyncHook(['arg1', 'arg2']) } // 给自定义 hook 注册事件。 // 同步 Hook 可以使用 tap 监听事件。其中,第一个参数是事件名,可以用作区分事件的标识符;第二个参数是调用自定义 hook 注册的事件时会被执行的回调函数。可以给一个 hook 注册多个事件,当调用自定义 hook 注册的事件时,都会被触发。 this.hooks.syncHook.tap('event1', (name, age) => { console.log(`event1:${name}、${age}`) }) this.hooks.syncHook.tap('event2', (name) => { console.log(`event2:${name}、${age}`) }) } emit() { // 调用自定义 hook 注册的事件。可以多次调用 this.hooks.syncHook.call('Lee', 18) this.hooks.syncHook.call('Mary', 20) } } const customPlugin = new CustomPlugin() customPlugin.emit()
- 运行
node ./src/index.js
命令运行该文件,可以看到效果。
不同关键字的作用:
bail :
bail:在某个事件监听的函数中,如果有返回值的话,后续监听的事件不会再被执行了。
const {SyncBailHook} = require('tapable')
class CustomPlugin {
constructor() {
this.hooks = {
syncBailHook: new SyncBailHook(['arg1', 'arg2'])
}
this.hooks.syncBailHook.tap('event1', (name, age) => {
console.log(`event1:${name}、${age}`)
return `${name}、${age}`
})
this.hooks.syncBailHook.tap('event2', (name, age) => {
console.log(`event2:${name}、${age}`)
})
}
emit() {
this.hooks.syncBailHook.call('Lee', 18)
}
}
const customPlugin = new CustomPlugin()
customPlugin.emit()
可以看到,在 event1
中返回值之后,event2
没有再被执行了。
loop:
loop:在某个事件监听的函数中,当返回值为 true,就会反复执行该事件;当返回值为 undefined 或者不返回内容,就退出事件。
waterfail:
waterfail:当有不为 undefined 的返回值时,会将这次返回的结果作为下次事件的第一个参数。
const {SyncWaterfallHook} = require('tapable')
class CustomPlugin {
constructor() {
this.hooks = {
syncWaterfallHook: new SyncWaterfallHook(['arg1', 'arg2'])
}
this.hooks.syncWaterfallHook.tap('event1', (name, age) => {
console.log(`event1:${name}、${age}`)
return 'event1'
})
this.hooks.syncWaterfallHook.tap('event2', (name, age) => {
console.log(`event2:${name}、${age}`)
})
}
emit() {
this.hooks.syncWaterfallHook.call('Lee', 18)
}
}
const customPlugin = new CustomPlugin()
customPlugin.emit()
可以看到,在 event1
中返回的值被作为了 event2
的第一个参数。
series:
series:在一个 Hook 中,监听了多次事件,其回调函数会串行执行,只有上一个监听事件的回调函数执行完,才会执行下一个监听事件的回调函数。
const {AsyncSeriesHook} = require('tapable')
class CustomPlugin {
constructor() {
this.hooks = {
asyncSeriesHook: new AsyncSeriesHook(['arg1', 'arg2'])
}
// 串行,只有上一个监听事件的回调函数执行完,才会执行下一个监听事件的回调函数
// 异步 Hook 可以使用 tapAsync 异步监听事件。最后一个参数是一个回调函数,当所有的注册事件都执行完成后,才会执行该回调函数
this.hooks.asyncSeriesHook.tapAsync('event1', (name, age, callback) => {
setTimeout(() => {
console.log(`event1:${name}、${age}`)
callback()
}, 3000)
})
this.hooks.asyncSeriesHook.tapAsync('event2', (name, age, callback) => {
setTimeout(() => {
console.log(`event2:${name}、${age}`)
callback()
}, 2000)
})
}
emit() {
this.hooks.asyncSeriesHook.callAsync('Lee', 18, () => {
console.log('所有事件监听都执行完了')
})
}
}
const customPlugin = new CustomPlugin()
customPlugin.emit()
可以看到,event1
执行完,才执行 event2
。
parallel:
parallel:并行。在一个 Hook 中,监听了多次事件,其回调函数会并行执行。
const {AsyncParallelHook} = require('tapable')
class CustomPlugin {
constructor() {
this.hooks = {
asyncParallelHook: new AsyncParallelHook(['arg1', 'arg2'])
}
// 并行,会同时执行多个回调函数
// 异步 Hook 也可以使用 tapPromise 异步监听事件
this.hooks.asyncParallelHook.tapPromise('event1', (name, age) => {
// 需要返回一个 Promise
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`event1:${name}、${age}`)
resolve()
}, 3000)
})
})
this.hooks.asyncParallelHook.tapPromise('event2', (name, age) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`event2:${name}、${age}`)
resolve()
}, 2000)
})
})
}
emit() {
this.hooks.asyncParallelHook.promise('Lee', 18).then(() => {
console.log('所有事件监听都执行完了')
})
}
}
const customPlugin = new CustomPlugin()
customPlugin.emit()
可以看到,由于 event2
异步任务的事件更短,会最先完成。
自定义 Plugin:
Plugin 如何注册到 Webpack 的生命周期中?
- 在 Webpack 函数的 createCompiler 方法中,注册了所有的插件。
- 在注册插件时,会调用插件函数或者插件对象的 apply 方法。
- 插件方法会接收到 compiler 对象,就可以通过 compiler 对象来注册 Hook 事件。
自定义 Plugin 的代码实现:
Webpack 中的大部分插件都是一个类,在类中需要实现一个叫做 apply 的方法;Webpack 会调用插件的 apply 方法,并将 compiler 编译器对象作为参数传入;在插件的 apply 方法中就可以通过 compiler 编译器对象获取到 Hook 并为其注册事件;当 Webpack 运行到对应的生命周期时会调用对应 Hook 注册的事件,其中回调函数就会被执行。
- 新建
src/index.js
,并编写代码。// src/index.js console.log('Hello World')
- 新建
src/AutoUploadPlugin.js
,并编写自定义插件的代码。// 自定义一个插件:功能是将静态文件自动上传到服务器中 class AutoUploadPlugin { // new AutoUploadPlugin() 时传入的参数 constructor(options) { this.options = options console.log(this.options) } // Webpack 在注册插件时,都会调用插件的 apply 方法,并且传入 compiler 编译器对象作为参数 apply(compiler) { // 为 afterEmit Hook 注册事件,当 Webpack 运行到这个生命周期时调用这个 Hook 注册的事件,回调函数就会被执行 // afterEmit 是 Webpack 资源输出完成之后的 Hook,可以获取到 compilation 对象作为其回调函数的参数。 compiler.hooks.afterEmit.tapAsync('AutoUploadPlugin', (compilation, callback) => { console.log('在此处进行静态文件上传服务器的操作') callback() }) } } module.exports = AutoUploadPlugin
- 在
webpack.config.js
配置文件中使用自定义插件。const AutoUploadPlugin = require('./src/AutoUploadPlugin') module.exports = { mode: 'development', plugins: [ new AutoUploadPlugin({ host: '123.123.123', port: '8080', username: '用户名', passowrd: '密码', remotePath: '/dist' }) ] }
- 运行
webpack
命令进行打包,会发现,插件被执行了。
HtmlWebpackPlugin 源码查看:
可以看到,HtmlWebpackPlugin 插件也是在 HtmlWebpackPlugin 类中实现了 apply 方法。