在现代前端开发中,webpack 已经成为了一个不可或缺的工具;
开发过程中,大家也或多或少接触过 webpack 中各式各样的插件。
可以说 插件系统赋予了 webpack 丰富的可扩展性 ,让我们可以根据项目需求定制化地配置和扩展 webpack 的功能。
今天我们就用一杯 ☕ 的时间,教你实现一个简单的 webpack 插件!
插件的本质
要想学会编写插件,首先得弄明白 webpack 插件到底是什么。
apply 方法
其实很简单,webpack 的插件本质上就是一个 拥有 apply
方法的函数或类;
不信?那我们扒几个插件源码瞅瞅:
-
html-webpack-plugin
: -
copy-webpack-plugin
从上面的代码中可以看出,这些插件中都实现了 apply
方法;
当 webpack 在初始化时就会调用这个 apply
方法,并传入一个参数 compiler
对象。
这里的 compiler
包含了 当前 webpack 环境的各种配置信息,以及关于模块和编译的所有信息。
其中,对于插件来说最重要的就是 上面挂载了许多的 hook ,通过 compiler.hooks.钩子名称
的语法,我们就能拿到对应的 hook 来进行一系列操作。
hook 介绍
hook 就类似于我们写 Vue 时的生命周期函数。
webpack 会在不同的构建流程中触发对应的 hook,并传入不同的上下文参数;
可以说,编写插件的过程就是 ------ 找到对应的 hook 并注册,然后在回调函数中使用传入的上下文参数实现逻辑的过程,比如:
js
// 通过 compiler.hooks.compilation 拿到对应钩子
// 通过 tap 方法来注册回调
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
// 在回调函数中拿到参数 compilation
// 执行一些逻辑
});
webpack 提供了几百种 hook ,我们来简单了解几个 hook 的使用:
compiler.hooks.compilation
触发时机
webpack 在每次编译过程中都会新建一个 compilation
对象,这个钩子就会在 compilation
对象新建时被触发;
上下文参数:
这个钩子的上下文参数是当前编译期间的 compilation
对象;同时,compilation
对象上又挂载了其它的许多钩子可供调用:
js
// 在每次新建 Compilation 对象时触发
compiler.hooks.compilation.tap('FirstPlugin', (compilation) => {
// 这个钩子在 webpack 处理 Chunk ID 的阶段触发
compilation.hooks.chunkIds.tap('SecondPlugin', () => {})
});
compiler.hooks.emit
触发时机
当 webpack 即将生成文件(将要输出资源到输出目录)时,compiler.hooks.emit
钩子会被触发。
上下文参数:
它的参数同样是当前编译期间的 compilation
对象。
compiler.hooks.done
触发时机
当 webpack 完成编译、打包并且输出结果后触发。
上下文参数:
这个钩子的上下文参数是 stats
,也就是本次 编译的相关信息;
通过监听这个钩子,我们就可以做输出日志、通知其他系统、执行清理等操作。
webpack 完整 hook 目录戳这里 👉 webpack hook
hook 类型
了解了 webpack 提供了哪些 hook,你就已经掌握了一大半插件的工作原理啦。
现在不妨来思考一个问题:如果我们在一个 hook 上注册了多个回调函数,那么这些回调函数会被如何调用呢?
比如说,下面这段代码它的输出顺序是什么:
js
compiler.hooks.compile.tap('FirstPlugin',
() => console.log('FirstPlugin')
);
compiler.hooks.compile.tap('SecondPlugin',
() => console.log('SecondPlugin')
);
实际上,上面这段代码的输出顺序取决于 hook 的类型。
对于 compile
这个 hook 来说,它是一个 同步的钩子(SyncHook) ,因此它的输出顺序就是 先输出 'FirstPlugin' ,然后输出 'SecondPlugin';
对于不同的类型的 hook,还提供不同的注册方式来注册回调函数。
下面我们一起来看看 webpack 中具体有哪些类型的 hook:
同步钩子(SyncHook)
这些钩子用于同步操作,它们的回调函数会 按照注册的顺序依次执行。
- 注册方式:只能使用
tap
方法; - 相应 hook :
compile
、thisCompilation``compilation
等等;
js
compiler.hooks.compile.tap('MyPlugin', (params) => {
console.log('我是一个同步钩子');
});
同步熔断钩子(SyncBailHook)
这些钩子也是同步的。但如果 回调函数返回非 undefined
的值,钩子的执行将停止,后续的回调函数将不会被调用。
- 注册方式:只能使用
tap
方法; - 相应 hook :
entryOption
、shouldEmit
、log
等等;
js
compiler.hooks.shouldEmit.tap('MyPlugin', (compilation) => {
//返回 false 会阻断后续回调的调用
return false;
});
同步瀑布流钩子(SyncWaterfallHook)
这些钩子是 同步的。它们的每个监听函数都会按照注册的顺序被调用,并 将前一个回调函数的返回值传递给下一个回调函数。
- 注册方式:只能使用
tap
方法; - 相应 hook :
assetPath
、contextModuleFiles
;
js
compiler.hooks.thisCompilation.tap('MyPlugin', (compilation) => {
compilation.hooks.assetPath.tap(
'MyPlugin',
(filename, data) => {
// 将自定义的文件名传给下一个回调函数
return 'custom_prefix_' + filename;
}
);
});
异步并行钩子(AsyncParallelHook)
这些钩子支持异步操作,回调函数可以 并行执行。
回调函数必须 调用入参中的 callback
或返回 Promise
。
- 注册方式:可以使用
tap
、tapAsync
、tapPromise
方法。- 对于使用
tapAsync
注册的回调函数,Webpack 会 等待所有回调函数调用callback
方法之后才会继续执行。 - 对于使用
tapPromise
注册的回调函数,Webpack 会 等待所有返回的Promise
都resolve
之后才会继续执行。
- 对于使用
- 相应 hook :
make
。
js
compiler.hooks.make.tapAsync('MyPlugin', (compilation, callback) => {
callback();
});
// 或者
compiler.hooks.afterEmit.tapPromise('MyPlugin', (compilation) => {
return new Promise((resolve) => {
resolve();
});
});
异步串行钩子(AsyncSeriesHook)
这些钩子也支持异步操作,但回调函数会 按照注册的顺序一个接一个依次执行。
- 注册方式:可以使用
tap
、tapAsync
、tapPromise
方法。调用逻辑同异步并行钩子(AsyncParallelHook)。 - 相应 hook :
run
、emit
、afterEmit
、assetEmitted
等等。
异步瀑布流钩子(AsyncSeriesWaterfallHook)
这些钩子支持异步操作,它们的每个回调函数都会 按照注册的顺序被调用,并将前一个回调函数解析的值传递给下一个监听函数。
- 注册方式:可以使用
tap
、tapAsync
、tapPromise
方法。调用逻辑同异步并行钩子(AsyncParallelHook)。 - 相应 hook :
beforeResolve
、afterResolve
、alternativeRequests
等等。
小试牛刀
到这里,你已经完全掌握了 webpack 插件的奥秘!
小伙伴们不妨动动手指来试着编写一个插件,用来在 webpack 编译完成后输出相应的编译信息。
下面是具体的插件代码:
js
// CompileInfoPlugin.js
const fs = require('fs')
const path = require('path')
class BuildInfo {
apply (compiler) {
// 通过 done 这个钩子注册一个在编译完成后执行的钩子
// 这里回调函数中的入参 stats 就是本次编译相关信息
compiler.hooks.done.tap('CompileInfoPlugin', (stats) => {
// 编译完成后输出一条信息
console.log('Compile is done.')
// 将编译信息输出到 dist 目录下的 report 文件中
const filepath = path.join(process.cwd(), './dist/report')
const str = stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
})
fs.writeFileSync(filepath, str)
})
}
}
// 导出模块
module.exports = {
BuildInfo
}
接下来在 webpack 配置中使用上面的插件:
js
// webpack.config.js
const CompileInfoPlugin = require('./CompileInfoPlugin');
module.exports = {
// ... 其他配置项
plugins: [
new CompileInfoPlugin()
]
};
大功告成!🎉🎉🎉