本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-02-26
在Webpack
运行的生命周期中会广播许多事件, Plugin
可以监听这些事件,在合适的时机通过 Webpack
提供的API
改变输出结果。
一个最基础的Plugin
代码如下:
class BasicPlugin {
// 在构造函数中获取用户为该插件传入的配置
constructor(options) {
this.options = options
},
// Webpack会调用BasicPlugin实例的apply方法为插件实例传入compiler对象
apply(compiler) {
compiler.plugin('compilation', function(compilation){
// ... 监听到compilation事件后的一系列操作
})
}
}
// 导出
module.exports = BasicPlugin;
在 Webpack
的配置中这样使用
const BasicPlugin = require('./BasicPlugin.js');
module.exports = {
Plugins: [
new BasicPlugin(options),
]
}
Webpack
启动后,在读取配置的过程中会先执行new BasicPlugin(options)
,初始化一个BasicPlugin
并获得其实例。在初始化Compiler
对象后,再调用basicPlugin.apply(compiler)
为插件实例传入compiler
对象。插件实例在获取到compiler
对象后,就可以通过compiler.plugin(事件名称,回调函数)
监听到Webpack
广播的事件,并且可以通过compiler
对象去操作Webpack
。
Compiler
对象包含了 Webpack
环境的所有配置信息,包含options
、loaders
、plugins
等信息。这个对象在Webpack
启动时被实例化,它是全局唯一的,可以简单地将它理解为 Webpack
实例。
Compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack
以开发模式运行时,每当检测到一个文件发生变化,便有一次新的Compilation
被创建。Compilation
对象也提供了很多事件回调供插件进行扩展。通过Compilation
也能读取到Compiler
对象。
Compiler
和 Compilation
的区别在于: Compiler
代表了整个 Webpack
从启动到关闭的生命周期,而Compilation
只代表一次新的编译。
使用了观察者模式,插件中进行订阅,Webpack
进行发布(开发的插件中也能发布其他事件)。
// 发布
compiler.apply('event-name', params);
// 插件中订阅的事件回调就会被调用
compiler.plugin('event-name', function(params){
// 监听到了事件的发布 该回调就会调用。
})
只要能拿到 Compiler
或 Compilation
对象,就能广播新的事件,所以在新开发的插件中也能广播事件,为其他插件监听使用。传给每个插件的Compiler
和Compilation
对象都是同一个引用。也就是说,若在一个插件中修改了Compiler
或Compilation
对象上的属性,就会影响到后面的插件。
有些事件是异步的,这些异步的事件会附带两个参数,第2个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack
,才会进入下一个处理流程。
compiler.plugin('emit', function(compilation, callback){
// 处理完毕后执行callback 以通知Webpack
// 如果不执行callback ,运行流程将会一直卡在这里而不往后执行
callback();
})
emit
事件发生时,代表源文件的转换和组装己经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。插件的代码如下:
class Plugin {
constructor() {
},
apply(complier) {
complier.plugin('emit', function(compilation, callback){
// compilation.chunks : Array 存放所有代码块
compilation.chunks.forEach(function(chunk) {
// 而chunk本身又是由很多个Modules组成的,所以也是二维数组
// 可以使用forEachModule来遍历
chunk.forEachModule(function (module) {
// module就是chunk中每一个单独的模块
// module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
module.fileDependencies.forEach(function(filePath){
// filePath 就是 module中每一个依赖文件的路径
})
})
// Webpack 会根据Chunk 生成输出的文件资源,每个Chunk 都对应一个及以上的输出文件
// 例如在chunk 中包含css 模块并且使用了ExtractTextPlugin 时
// 该chunk 就会生成.js 和 .css 两个文件
// chunk.files 就是该chunk将要输出的文件名称组成的数组
chunk.files.forEach(function(filename){
// compilation.assets 存放着当前即将输出的所有资源
// 通过该文件名在compilation.assets中获取当前输出资源,调用一个输出资源的source()方法能获取输出资源的内容
let source = compilation.assets[filename].source();
})
})
// 这是一个异步事件,要记得调用callback 来通知Webpack 本次事件监听处理结束
callback();
})
}
}
Webpack
会从配置的入口模块出发,依次找出所有依赖模块, 当入口模块或者其依赖的模块发生变化时, 就会触发一次新的Compilation
。
在开发插件时经常需要知道是哪个文件发生的变化导致了新的Compilation
,可以监听watch-run
事件,当依赖的文件发生变化时会被触发。
compiler.plugin('watch-run', function(watching, callback){
// 获取发生变化的文件列表
const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
// changedFiles 格式为键值对,键为发生变化的文件路径
if (changedFiles['./show.js'] !== undefined) {
// show.js 发生了变化
}
// 异步事件
callback();
})
Webpack
只会监视入口和其依赖的模块是否发生了变化,所以Webpack
不会监听HTML
文件的变化,编辑HTML
文件时就不会重新触发新的Compilation
。某些情况下会需要监听HTML的变化,这时候可以将HTML
文件加入依赖列表中,为此可以使用如下代码:
// after-compile 会在一次Compilation 执行完成
comper.plugin('after-compile', (compilation, callback) => {
// 将HTML 文件添加到文件依赖列表中,因此在HTML 模板文件发生变化时重新启动一次编译
compilation.fileDependencies.push('./test.html');
callback();
})
在某些场景下插件需要修改、增加、删除输出的资源,要做到这一点, 则需要监听emit
事件,因为发生emit
事件时所有模块的转换和代码块对应的文件已经生成好,需要输出的资源即将输出,因此emit
事件是修改 Webpack
输出资源的最后时机。
上面已提到过 compilation.assets
存放着当前即将输出的所有资源,是一个键值对,键为需要输出的文件名称,值为文件对应的内容。
// 修改输出文件
compiler.plugin('emit', (compilation, callback) => {
// 设置名称为fileName 的输出资源
compilation.assets[fileName] = {
// source()用来输出文件内容,在source方法中对文件进行修改并输出
source: () => {
// 文件内容 既可以是代表文本文件的字符串,也可以是代表二进制文件的Buffer
return fileContent;
},
// 返回文件的大小
size: () => {
return Buffer.byteLength(fileContent, 'utf8');
}
}
callback();
})
// 读取输出文件
compiler.plugin('emit', (compilation, callback) => {
// 读取名称为fileName 的输出资源
const asset = compiler.assets[fileName];
// 获取输出的内容
asset.source();
// 获取输出资源的大小
asset.size();
callback();
})
在开发一个插件时,我们可能需要根据当前配置是否使用了其他插件来做下一步决定,因此需要读取Webpack
当前的插件配置情况。比如,若想判断当前是否使用了ExtractTextPlugin
function hasExtractTextPlugin(compiler) {
// 当前配置使用的所有插件列表
const plugins = compiler.options.plugins;
const tmp = plugins.find(plugin => {
// 去plugins 中寻找有没有ExtractTextPlugin 的实例
return plugin.__proto__.constructor === ExtractTextPlugin
});
return tmp != null;
}
该插件的名称为 EndWebpackPlugin
,作用是在Webpack
即将退出时 针对构建成功还是构建失败 附加一些额外的操作。
// 如何使用
module.exports = {
Plugins: [
// 在初始化的时候,传入两个参数,分别会成功回调和失败回调
new EndWebpackPlugin(() => {
// Webpack 构建成功,并且在文件输出后会执行到这里,在这里可以做发布文件操作
}, (err) => {
// Webpack 构建失败, err 是导致错误的原因
console.log(err);
})
]
}
// 插件代码
class EndWebpackPlugin {
constructor(doneCb, failCb) {
this.doneCb = doneCb;
this.failCb = failCb;
},
apply(compiler) {
compiler.plugin('done', function(stats){
// 监听webpack 的 done 事件,回调doneCb
this.doneCb(stats)
})
compiler.plugin('failed', function(err){
// 监听webpack 的 done 事件,回调doneCb
this.failCb(err)
})
}
}
module.exports = EndWebpackPlugin;