webpack之钩子流程梳理

424 阅读2分钟

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内部会自动执行注册的事件,这个很错综复杂,暂时没理解时机的对应关系

参考文章:【webpack进阶】可视化展示webpack内部插件和钩子关系