本质上在 Webpack 编译阶段会为各个编译对象初始化不同的 Hook ,开发者可以在自己编写的 Plugin 中监听到这些 Hook ,在打包的某个特定时间段触发对应 Hook 注入特定的逻辑从而实现自己的行为。
可以换句话定义,就是 Plugin 内部完全是基于 tapable 来实现,由于 tapable 事件流机制过于硬核,这里就不细说,只要理解为一个事件监听的类即可
我们来确认 plugin 的实现要求
- 一个 JavaScript 命名函数或者一个类
- 在插件函数的 prototype 或者类上定义一个 apply 方法(方法参数是 compiler 对象)
- 指定一个绑定到 webpack 自身的事件钩子 (opens new window)
- 处理 webpack 内部实例的特定数据
- 功能完成后调用 webpack 提供的 callback 回调函数
基本构成
js
class ExamplePlugin {
// 在构造函数中获取用户给该插件传入的配置
constructor(options){
//
}
// Webpack 会调用 ExamplePlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) {
// 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
compiler.hooks.emit.tap('ExamplePlugin', (compilation) => {
// 在功能流程完成后可以调用 webpack 提供的回调函数;
});
// 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知webpack,才会进入下一个处理流程。
compiler.plugin('emit',function(compilation, callback) {
// 支持处理逻辑
// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这不往下执行
callback();
});
}
}
module.exports = ExamplePlugin;
-
在配置文件中,Webpack 在读取配置的过程中会先执行 new ExamplePlugin(options) 获取实例对象
-
在初始化阶段,Webpack 会逐个调用每个 plugin 的 apply 方法给插件实例传入 compiler 对象
-
插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(eventName, callback) 监听到 Webpack 广播出来的事件,并且可以通过 compiler 对象去操作 Webpack
读取输出资源、代码块、模块及其依赖
在 emit 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容
js
class ExamplePlugin {
apply(compiler) {
// 异步事件,需要调用 callback 回调函数通知 webpack,才会进入下一个处理流程。
compiler.plugin('emit', function (compilation, callback) {
// compilation.chunks 存放所有代码块,是一个数组
compilation.chunks.forEach(function (chunk) {
// chunk 代表一个代码块
// 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块
chunk.forEachModule(function (module) {
// module 代表一个模块
// module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
module.fileDependencies.forEach(function (filepath) {
});
});
// Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
// 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
// 该 Chunk 就会生成 .js 和 .css 两个文件
chunk.files.forEach(function (filename) {
// compilation.assets 存放当前所有即将输出的资源
// 调用一个输出资源的 source() 方法能获取到输出资源的内容
const source = compilation.assets[filename].source();
});
});
// 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
callback();
})
}
}
监听文件变化
Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation
这里提一嘴:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译,只要文件有改动,Compilation就会被重新创建
在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation,为此可以使用如下代码
js
// 当依赖的文件发生变化时会触发 watch-run 事件
compiler.hooks.watchRun.tap('ExamplePlugin', (watching, callback) => {
// 获取发生变化的文件列表
const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
// changedFiles 格式为键值对,键为发生变化的文件路径。
if (changedFiles[filePath] !== undefined) {
// filePath 对应的文件发生了变化
}
callback();
});
修改输出资源
当我们需要修改、增加或者删除输出的资源,我们就需要监听 emit 事件。emit 事件执行时,所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此 emit 事件是修改 Webpack 输出资源的最后时机
所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容
js
// 设置名称为 fileName 的输出资源
compilation.assets[fileName] = {
// 返回文件内容
source: () => {
// fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
return fileContent;
},
// 返回文件大小
size: () => {
return Buffer.byteLength(fileContent, 'utf8');
}
};
callback();
如何判断使用了哪些插件
js
// 判断当前配置使用使用了 ExtractTextPlugin,
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExamplePlugin(compiler) {
// 当前配置所有使用的插件列表
const plugins = compiler.options.plugins;
// 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
return plugins.find(plugin => plugin.__proto__.constructor === ExamplePlugin) != null;
}
终端不中断编译进行打印警告
如果你在 apply 函数内插入 throw new Error("Message"),终端会提示报错,并将 webpack 中断处理,为了不影响 webpack 的执行,要在编译期间向用户发出警告或错误消息,则应使用 compilation.warnings 和 compilation.errors
js
compilation.warnings.push("warning");
compilation.errors.push("error");
实战
我们来实现一个 FileListTxtPlugin 插件,实现将 webpack 打印的文件相关信息枚举出来,并生成一份清单输出到打包目录下。
根据上文讲解,我们可以通过 emit hook 来进行
- 初始化插件文件
js
class FileListTxtPlugin {
// apply函数 帮助插件注册,接收complier类
constructor(options) {
// webpack 中配置的 options 对象
this.options = options;
}
apply(complier) {
// 需要读取和写入文件,这里就用异步事件
complier.hooks.emit.tapAsync(() => {
// ...
})
}
}
export default FileListTxtPlugin
- 插入逻辑
js
class FileListTxtPlugin {
// apply函数 帮助插件注册,接收complier类
constructor(options) {
// webpack 中配置的 options 对象
this.options = options;
}
apply(complier) {
// 异步的钩子
complier.hooks.emit.tapAsync(
'FileListTxtPlugin',
(compilation, callback) => {
// 获取所有文件路径
const fileDependencies = [...compilation.fileDependencies];
// 打包后 dist 目录下的文件资源都放在 assets 对象中
const assets = compilation.assets;
// 定义返回文件的内容
let fileContent = `文件数量:${Object.keys(assets).length}\n文件列表:`;
// 遍历 dist 目录下的所有资源
Object.keys(assets).forEach((item) => {
// 文件的源内容
const source = assets[item].source();
// 文件的大小
let size = assets[item].size();
// 如果大于 1024 byte,则用 kb
size =
size >= 1024 ? `${(size / 1024).toFixed(2)}/kb` : `${size}/bytes`;
// 获取对应的文件路径
const sourcepath =
fileDependencies.find((path) => {
if (path.includes(item)) return path;
}) || '';
// 文件内容追加
fileContent = `${fileContent}\n filename: ${item} size: ${size} ${sourcepath}`;
});
// 添加自定义输出文件,这里是添加文件输出,这里可以修改替换对应的文件
compilation.assets['fileList.txt'] = {
source: function () {
// 定义文件的内容
return fileContent;
},
size: function () {
// 定义文件的体积
return Buffer.byteLength(fileContent, 'utf8');
},
};
// 注意,异步钩子中 callback 函数必须要调用
callback();
}
);
}
}
module.exports = FileListTxtPlugin;
- 注册插件
js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const FileListTxtPlugin = require('./plugins/fileListTxtPlugin');
module.exports = {
entry: './src/index.js',
mode: 'development',
// mode: 'production',
output: {
clean: true, // 在生成文件之前清空 output 目录
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
},
module: {
rules: [
// ...
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
}),
new FileListTxtPlugin(),
],
};