【前端必会】tapable、hook,webpack的灵魂

背景

  1. 什么是tapable、hook,平时做vue开发时的webpack 配置一直都没弄懂,你也有这种情况吗?
  2. 还是看源码,闲来无聊又看一下webpack的源码,看看能否找到一些宝藏
  3. tapable和webpack没有特定关系,可以先看下这篇文章,了解下这个小型库
    https://webpack.docschina.org/api/plugins/#tapable
    https://blog.csdn.net/mafan121/article/details/113120081
    4.下面记录下寻宝过程

开始

执行一次webpack经历了什么,先看一下代码
【前端必会】tapable、hook,webpack的灵魂插图

我们分析一下4点

  1. 引用了webpack
  2. 我们使用的配置文件
  3. 调用webpack函数,传入配置,返回一个compiler(编译器)
  4. 执行编译器的run方法

分析

引用webpack,先把这个函数找出来
https://github.com/webpack/webpack/blob/main/package.json
"main": "lib/index.js",

https://github.com/webpack/webpack/blob/main/lib/index.js

module.exports = mergeExports(fn, {
    get webpack() {
        return require("./webpack");
    },

https://github.com/webpack/webpack/blob/main/lib/webpack.js

const webpack = /** @type {WebpackFunctionSingle & WebpackFunctionMulti} */ (
    /**
     * @param {WebpackOptions | (ReadonlyArray & MultiCompilerOptions)} options options
     * @param {Callback & Callback=} callback callback
     * @returns {Compiler | MultiCompiler}
     */
    (options, callback) => {
        const create = () => {
            if (!asArray(options).every(webpackOptionsSchemaCheck)) {
                getValidateSchema()(webpackOptionsSchema, options);
                util.deprecate(
                    () => {},
                    "webpack bug: Pre-compiled schema reports error while real schema is happy. This has performance drawbacks.",
                    "DEP_WEBPACK_PRE_COMPILED_SCHEMA_INVALID"
                )();
            }
            /** @type {MultiCompiler|Compiler} */
            let compiler;
            let watch = false;
            /** @type {WatchOptions|WatchOptions[]} */
            let watchOptions;
            if (Array.isArray(options)) {
                /** @type {MultiCompiler} */
                compiler = createMultiCompiler(
                    options,
                    /** @type {MultiCompilerOptions} */ (options)
                );
                watch = options.some(options => options.watch);
                watchOptions = options.map(options => options.watchOptions || {});
            } else {
                const webpackOptions = /** @type {WebpackOptions} */ (options);
                /** @type {Compiler} */
                compiler = createCompiler(webpackOptions);
                watch = webpackOptions.watch;
                watchOptions = webpackOptions.watchOptions || {};
            }
            return { compiler, watch, watchOptions };
        };
        if (callback) {
            try {
                const { compiler, watch, watchOptions } = create();
                if (watch) {
                    compiler.watch(watchOptions, callback);
                } else {
                    compiler.run((err, stats) => {
                        compiler.close(err2 => {
                            callback(err || err2, stats);
                        });
                    });
                }
                return compiler;
            } catch (err) {
                process.nextTick(() => callback(err));
                return null;
            }
        } else {
            const { compiler, watch } = create();
            if (watch) {
                util.deprecate(
                    () => {},
                    "A 'callback' argument needs to be provided to the 'webpack(options, callback)' function when the 'watch' option is set. There is no way to handle the 'watch' option without a callback.",
                    "DEP_WEBPACK_WATCH_WITHOUT_CALLBACK"
                )();
            }
            return compiler;
        }
    }
);

module.exports = webpack;

这里主要就是调用create创建一个Compiler(先不理watch)
【前端必会】tapable、hook,webpack的灵魂插图1

在看一下create,这里是调用的createCompiler或者createMultiCompiler
【前端必会】tapable、hook,webpack的灵魂插图2

在看一下createCompiler,这里主要就是new一个Compiler。这个时候已经开始了webpack编译的生命周期。

/**
 * @param {WebpackOptions} rawOptions options object
 * @returns {Compiler} a compiler
 */
const createCompiler = rawOptions => {
    const options = getNormalizedWebpackOptions(rawOptions);
    applyWebpackOptionsBaseDefaults(options);
    const compiler = new Compiler(options.context, options);
    new NodeEnvironmentPlugin({
        infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    if (Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }
    applyWebpackOptionsDefaults(options);
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();
    return compiler;
};

我们简单看一下Compiler类的一些hooks

const {
    SyncHook,
    SyncBailHook,
    AsyncParallelHook,
    AsyncSeriesHook
} = require("tapable");

......

class Compiler {
    /**
     * @param {string} context the compilation path
     * @param {WebpackOptions} options options
     */
    constructor(context, options = /** @type {WebpackOptions} */ ({})) {
        this.hooks = Object.freeze({
            /** @type {SyncHook} */
            initialize: new SyncHook([]),

            /** @type {SyncBailHook} */
            shouldEmit: new SyncBailHook(["compilation"]),
            /** @type {AsyncSeriesHook} */
            done: new AsyncSeriesHook(["stats"]),
            /** @type {SyncHook} */
            afterDone: new SyncHook(["stats"]),
            /** @type {AsyncSeriesHook} */
            additionalPass: new AsyncSeriesHook([]),
            /** @type {AsyncSeriesHook} */
            beforeRun: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook} */
            run: new AsyncSeriesHook(["compiler"]),
            /** @type {AsyncSeriesHook} */
            emit: new AsyncSeriesHook(["compilation"]),
            /** @type {AsyncSeriesHook} */
            assetEmitted: new AsyncSeriesHook(["file", "info"]),
            /** @type {AsyncSeriesHook} */
            afterEmit: new AsyncSeriesHook(["compilation"]),

每个hook都是一个 tapable包里对应后hook的实例

在回到创建编译器那里,这时创建一个插件的实例,并且执行apply方法,插件就会向自己关系的hook添加事件处理函数(其实还是一个事件监听),NodeEnvironmentPlugin代码可以自行在源码中查看

new NodeEnvironmentPlugin({
        infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);

一切都准备好了之后,我们再看一下编译器的run方法

/**
     * @param {Callback} callback signals when the call finishes
     * @returns {void}
     */
    run(callback) {
        if (this.running) {
            return callback(new ConcurrentCompilationError());
        }

        let logger;

        const finalCallback = (err, stats) => {
            if (logger) logger.time("beginIdle");
            this.idle = true;
            this.cache.beginIdle();
            this.idle = true;
            if (logger) logger.timeEnd("beginIdle");
            this.running = false;
            if (err) {
                this.hooks.failed.call(err);
            }
            if (callback !== undefined) callback(err, stats);
            this.hooks.afterDone.call(stats);
        };

        const startTime = Date.now();

        this.running = true;

        const onCompiled = (err, compilation) => {
            if (err) return finalCallback(err);

            if (this.hooks.shouldEmit.call(compilation) === false) {
                compilation.startTime = startTime;
                compilation.endTime = Date.now();
                const stats = new Stats(compilation);
                this.hooks.done.callAsync(stats, err => {
                    if (err) return finalCallback(err);
                    return finalCallback(null, stats);
                });
                return;
            }

            process.nextTick(() => {
                logger = compilation.getLogger("webpack.Compiler");
                logger.time("emitAssets");
                this.emitAssets(compilation, err => {
                    logger.timeEnd("emitAssets");
                    if (err) return finalCallback(err);

                    if (compilation.hooks.needAdditionalPass.call()) {
                        compilation.needAdditionalPass = true;

                        compilation.startTime = startTime;
                        compilation.endTime = Date.now();
                        logger.time("done hook");
                        const stats = new Stats(compilation);
                        this.hooks.done.callAsync(stats, err => {
                            logger.timeEnd("done hook");
                            if (err) return finalCallback(err);

                            this.hooks.additionalPass.callAsync(err => {
                                if (err) return finalCallback(err);
                                this.compile(onCompiled);
                            });
                        });
                        return;
                    }

                    logger.time("emitRecords");
                    this.emitRecords(err => {
                        logger.timeEnd("emitRecords");
                        if (err) return finalCallback(err);

                        compilation.startTime = startTime;
                        compilation.endTime = Date.now();
                        logger.time("done hook");
                        const stats = new Stats(compilation);
                        this.hooks.done.callAsync(stats, err => {
                            logger.timeEnd("done hook");
                            if (err) return finalCallback(err);
                            this.cache.storeBuildDependencies(
                                compilation.buildDependencies,
                                err => {
                                    if (err) return finalCallback(err);
                                    return finalCallback(null, stats);
                                }
                            );
                        });
                    });
                });
            });
        };

        const run = () => {
            this.hooks.beforeRun.callAsync(this, err => {
                if (err) return finalCallback(err);

                this.hooks.run.callAsync(this, err => {
                    if (err) return finalCallback(err);

                    this.readRecords(err => {
                        if (err) return finalCallback(err);

                        this.compile(onCompiled);
                    });
                });
            });
        };

        if (this.idle) {
            this.cache.endIdle(err => {
                if (err) return finalCallback(err);

                this.idle = false;
                run();
            });
        } else {
            run();
        }
    }

https://github.com/webpack/webpack/blob/main/lib/Compiler.js

这里简单分析一下,主要就是执行run相关生命周期,以及编译。并且编译完成后传入回调函数onCompiled

const run = () => {
            this.hooks.beforeRun.callAsync(this, err => {
                if (err) return finalCallback(err);

                this.hooks.run.callAsync(this, err => {
                    if (err) return finalCallback(err);

                    this.readRecords(err => {
                        if (err) return finalCallback(err);

                        this.compile(onCompiled);
                    });
                });
            });
        };

整体逻辑不是很复杂,我们主要可以感受到webpack启动后对hook的一些使用方式。整体的逻辑差不多都是一样的。是不是很简单。

总结

  1. 想了解一个框架,一定要找到入口函数,一点一点向前探索。
  2. tapable 是个好东西,
  3. 关于webpack生命周期,有疑问的时候,除了看文档意外,还可以结合源码去理解,去感受

你学会了吗?欢迎留下你的感受!

文章来源于互联网:【前端必会】tapable、hook,webpack的灵魂

THE END
分享
二维码