Loader帮助我们去处理一类文件,Plugin又是什么呢?如果对webpack的基础知识不太熟悉,可以查看笔者的另一篇博客。
plugin: 在我们打包的过程中,到了某一个特定节点,该插件生效。
比如说,我们使用HtmlWebpackPlugin,这个插件会在打包完成之后,简单的创建html文件,可用于服务器的访问。
webpack的plugin 采用的是发布订阅模式,在这个模式下,代码的执行是通过事件来驱动的。
构建一个简单的plugin
我们想在打包完成之后再向dist
目录增加一个文件,里面包含了版权信息和一些打包信息。
前面我们学了loader,loader实际是就是一个导出为函数的JavaScript模块。plugin和loadedr不一样,它实际是一个类,就也是为什么我们在webpack.config.js中使用一个插件时需要new的原因。
在./plugins
目录下创建 info-webpack-plugin.js:
class InfoWebpackPlugin{
constructor() {
}
apply(compiler){
}
}
module.exports = InfoWebpackPlugin;
apply()就是该模块发挥作用是使用的方法。
然后在webpack.config.js中使用这个插件:
const path = require('path')
const InfoWebpackPlugin = require('./plugins/info-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins:[
new InfoWebpackPlugin()
],
output: {
path: path.resolve(__dirname,'dist'),
filename: '[name].js'
}
}
当然,这时候**InfoWebpackPlugin 没有任何作用。
我们修改一下InfoWebpackPlugin **:
class InfoWebpackPlugin{
constructor() {
console.log('InfoWebpackPlugin is created!')
}
apply(compiler){
}
}
module.exports = InfoWebpackPlugin;
然后打包,我们发现控制台出现了这样的内容:
InfoWebpackPlugin is created!
Hash: d23f266366dabf54e803
Version: webpack 4.46.0
Time: 74ms
...
现在,我们逐步来实现我们的plugin。
首先,我们需要知道,apply()方法在安装插件时,会被webpack compiler调用一次。apply接收一个webpack compiler对象的引用,所以我们可以使用complier对象。
那么什么是compiler ?
Tapable:
Tapable事实上就是一个事件管理器,类似于 NodeJS 的 EventEmitter 类,专注于自定义事件的触发和处理。
Compier:
Compiler 也是我们所说的 Tapable 实例,他就是webpack的整体环境。通过这种实现机制,我们可以理解为,它混合(mix)了 Tapable类,来使实例也具备注册和调用插件功能。
插件机制事实上就是通过注册在Complier上,在运行时Compier会根据各种事件钩子,从而触发插件的注册函数。
Compilation:
Compilation 实例继承于 compiler。例如,compiler.compilation 是对所有 require 图(graph)中对象的字面上的编译。这个对象可以访问所有的模块和它们的依赖(大部分是循环依赖)。在编译阶段,模块被加载,封闭,优化,分块,哈希和重建等等。这将是任何编译操作中,重要的生命周期。
官方文档看起来比较难理解,compilation 对象代表了一次单一的版本构建和生成资源。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。编译对象也提供了很多关键点回调供插件做自定义处理时选择使用。
compiler和compilation之间的区别?
- compiler对象代表的是不变的webpack环境。
- compilation 对象检测的是随时可变的项目文件,只要文件有改动,compilation就会重新创建。
根据所使用的 钩子(hook) 和 tap 方法,插件可以以多种不同的方式运行。这个工作方式与 Tapable 提供的 hooks 密切相关。compiler hooks 分别记录了 Tapable 内在的钩子,指出哪些 tap 方法可用。
因此,根据你触发到 tap 事件,插件可能会以不同的方式运行。例如,当钩入 compile 阶段时,只能使用同步的 tap 方法:
compiler.hooks.compile.tap('MyPlugin', params => {
console.log('以同步方式触及 compile 钩子。');
})
然而,对于能够使用了 AsyncHook(异步钩子) 的 run,我们可以使用 tapAsync 或 tapPromise(以及 tap):
compiler.hooks.run.tapAsync('MyPlugin', (source, target, routesList, callback) => {
console.log('以异步方式触及 run 钩子。');
callback();
});
compiler.hooks有哪些钩子可以选择,以及该钩子是异步还是同步,我们都可以查看webpack的plugin api,里面有相关说明。
回到我们现在编写的plugin,我们需要在打包时生成一个文件到dist目录下,通过查找钩子,我们发现emit符合我们的期望。emit会在生成资源到 output 目录之前被驱动,又因为emit是异步钩子,所以,修改info-webpack-plugin.js:
class InfoWebpackPlugin{
constructor() {
console.log('InfoWebpackPlugin is created!')
}
apply(compiler){
compiler.hooks.emit.tabAsync('InfoWebpackPlugin',(comilation,cb)=>{
})
}
}
module.exports = InfoWebpackPlugin;
这里compiler.hooks.emit.tabAsync(plugin,func)
表示,当打包执行到emit阶段的时候,安装名为plugin的插件,此插件的效果是func的内容。
class InfoWebpackPlugin{
apply(compiler){
compiler.hooks.emit.tapAsync('InfoWebpackPlugin',(compilation,cb)=>{
compilation.assets['info.txt'] = {
source: function (){
return 'copyright by Tang at ' + new Date().toLocaleString()
},
size: function() {
return 40;
}
}
cb()
})
}
}
module.exports = InfoWebpackPlugin;
这里comilation.assets['info.txt'] = {}
向本次编译生成的资源中添加一个info.txt,{}
中的内容就是生成info.txt需要的一些配置。
最后调用cb()
告诉compilation此插件已经安装完毕。
如果是同步钩子,就简单很多:
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
console.log('compiler');
})
现在我们重新打包,最后生成的文件:
info.txt的内容:
copyright by Tang at 2022-2-23 3:35:28 ├F10: PM┤
注意,如果需要向plugin传入参数,可以直接使用构造函数constructor接收即可。