webpack的成功之处,不仅在于强大的打包构建能力,也在于它灵活的插件机制。
首先,webpack中有一个重要的类 —— Compiler,它创建了非常多的钩子(下文中的SyncBailHook之类的),这些钩子将会散落在“各地”(下文中的shouldEmit之类的)被调用(call)
// Compiler类中的部分钩子(总共有180个钩子之多)
this.hooks = {
/** @type {SyncBailHook<Compilation>} */
shouldEmit: new SyncBailHook(["compilation"]),
/** @type {AsyncSeriesHook<Stats>} */
done: new AsyncSeriesHook(["stats"]),
/** @type {AsyncSeriesHook<>} */
additionalPass: new AsyncSeriesHook([]),
/** @type {AsyncSeriesHook<Compiler>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compiler>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<Compilation>} */
emit: new AsyncSeriesHook(["compilation"]),
……
}
1. 怎么把事件注册到钩子上去,又怎么触发事件的呢?
const { SyncHook } = require('tapable');
const mySyncHook = new SyncHook(['name', 'age']);
// 为什么叫tap水龙头 接收两个参数,第一个参数是名称(备注:没有任何意义) 第二个参数是一个函数 接收一个参数 name这个name和上面的name对应 age和上面的age对应
mySyncHook.tap('1', function (name, age) {
console.log(name, age, 1)
return 'wrong' // 不关心返回值 这里写返回值对结果没有任何影响
});
mySyncHook.call('liushiyu', '18');
// 执行的结果
// liushiyu 18 1
从上面可以看出:每一个钩子都是一个构造函数,所有的构造函数都接收一个可选的参数。使用tap, tapPromise, tapAsync来把事件注册上去。使用call(非我们平常认识的那个call)来触发事件。
总结下:模块/插件与钩子的关系主要分为三类:
- 模块/插件「创建」钩子,如
this.hooks.say = new SyncHook(); - 模块/插件将方法「注册」到钩子上,如
obj.hooks.say.tap('one', () => {...}); - 模块/插件通过「调用」来触发钩子事件,如
obj.hooks.say.call()。
2. webpack是如何调用插件,将插件中的方法在编译阶段注册到钩子上的呢?
对于这个问题,webpack规定每个插件的实例,必须有一个.apply()方法,webpack打包前会调用所有插件的.apply()方法,插件可以在该方法中进行钩子的注册。
例子如下:
module.exports = class FileListTxtWebpackPlugin {
// apply函数 帮助插件注册,接收complier类
constructor(options) {
console.log(options);
// webpack 中配置的 options 对象
this.options = options
}
apply(complier) {
// 异步的钩子
complier.hooks.emit.tapAsync("FileListTxtWebpackPlugin", (compilation, callback) => {
const fileDependencies = [...compilation.fileDependencies]
// 打包后 dist 目录下的文件资源都放在 assets 对象中
const assets = compilation.assets
// 定义返回文件的内容
let fileContent = `文件数量:${Object.keys(assets).length}\n文件列表:`
Object.keys(assets).forEach(item => {
// 文件的源内容
const source = assets[item].source();
// 文件的大小
let size = assets[item].size()
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: ${sourcepath}`
})
// 添加自定义输出文件
compilation.assets["fileList.txt"] = {
source: function () {
// 定义文件的内容
return fileContent
},
size: function () {
// 定义文件的体积
return Buffer.byteLength(fileContent, 'utf8');
},
};
// 注意,异步钩子中 callback 函数必须要调用
callback();
});
}
}
而webpack内部会自动执行注册的事件,这个很错综复杂,暂时没理解时机的对应关系