在上一篇《从vuecli3学习webpack记录(一)vue-cli-serve机制》里面我提到了

compiler.hooks.done.tap('vue-cli-service serve', stats => { ...})

在webpack里面应该会经常看到compiler这个单词不陌生吧。

 // create compiler
    const compiler = webpack(webpackConfig)

webpackConfig顾名思义就是webpack的配置信息。
webpack方法的源码如下

const webpack = (options, callback) => {
    const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
    if (webpackOptionsValidationErrors.length) {
        throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
    }
    let compiler;
    if (Array.isArray(options)) {
        compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if (typeof options === "object") {
        options = new WebpackOptionsDefaulter().process(options);

        compiler = new Compiler(options.context);
        compiler.options = options;
        new NodeEnvironmentPlugin().apply(compiler);
        if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
              // 执行webpackConfig的每个插件,调用call/apply方法
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }
        compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if (callback) {
        if (typeof callback !== "function") {
            throw new Error("Invalid argument: callback");
        }
        if (
            options.watch === true ||
            (Array.isArray(options) && options.some(o => o.watch))
        ) {
            const watchOptions = Array.isArray(options)
                ? options.map(o => o.watchOptions || {})
                : options.watchOptions || {};
            return compiler.watch(watchOptions, callback);
        }
        compiler.run(callback);
    }
    return compiler;
};

可以看出webpack()返回的其实是一个Compiler/MultiCompiler(继承自Tapable)实例,只是在返回compiler前,进行了一些判断和处理。

vue.config.js

比如使用vue-cli3搭建的简单项目,并配置如下

在调用webpack方法生成compiler时的options就是该配置信息。

我们可以看到里面的plugins里面有不少(怎么比你想象中要多?这要归功于上一篇提到的builtInPlugins)。
传入的webpackConfig是对象(我们一般情况传递的配置文件都是此类型)或者数组的处理方式略有不同。

webpack方法

// node_modules/webpack/lib/webpack.js
let compiler;
if (Array.isArray(options)) {
    compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if (typeof options === "object") {
    options = new WebpackOptionsDefaulter().process(options);

    compiler = new Compiler(options.context);
    compiler.options = options;
    new NodeEnvironmentPlugin().apply(compiler);
    if (options.plugins && Array.isArray(options.plugins)) {
        for (const plugin of options.plugins) {
            if (typeof plugin === "function") {
                plugin.call(compiler, compiler);
            } else {
                plugin.apply(compiler);
            }
        }
    }
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
    throw new Error("Invalid argument: options");
}

插件执行

我们先看里面对插件的处理部分for (const plugin of options.plugins)。这时在生成compiler后会对plugins进行循环并调用各个插件。
这也就是为什么我们在插件时会要求有apply方法,在new一个插件实例的时候我们这里就可以成功调用它的apply了。

ProgressPlugin

如果脱离具体的插件示例来讲webpack是不好理解的,所以这里以我们常见的ProcessPlugin(就是我们在开发过程中的那个显示进度百分比的插件)为例。
它是webpack的内置插件,webpack的内置插件现在有很多,不少还是明星插件呢。

// node_modules/webpack/lib/ProcessPlugin.js
class ProgressPlugin {
    /**
     * @param {ProgressPluginArgument} options options
     */
    constructor(options) {
        if (typeof options === "function") {
            options = {
                handler: options
            };
        }

        options = options || {};
        validateOptions(schema, options, "Progress Plugin");
        options = Object.assign({}, ProgressPlugin.defaultOptions, options);

        this.profile = options.profile;
        this.handler = options.handler;
        this.modulesCount = options.modulesCount;
        this.showEntries = options.entries;
        this.showModules = options.modules;
        this.showActiveModules = options.activeModules;
    }
}

构造还是一般都是用来存储一些数据,方便实例方法调用的,这个插件在使用时我们一般都没有传递option,这是我们使用的就是插件的默认配置了。

ProgressPlugin.defaultOptions = {
    profile: false,
    modulesCount: 500,
    modules: true,
    activeModules: true,
    // TODO webpack 5 default this to true
    entries: false
};

插件的启动方法,也是核心方法apply

apply(compiler) {
    const { modulesCount } = this;
    const handler = this.handler || createDefaultHandler(this.profile);
    const showEntries = this.showEntries;
    const showModules = this.showModules;
    const showActiveModules = this.showActiveModules;
    if (compiler.compilers) {
        const states = new Array(compiler.compilers.length);
        compiler.compilers.forEach((compiler, idx) => {
            new ProgressPlugin((p, msg, ...args) => {
                states[idx] = [p, msg, ...args];
                handler(
                    states
                    .map(state => (state && state[0]) || 0)
                    .reduce((a, b) => a + b) / states.length,
                    `[${idx}] ${msg}`,
                    ...args
                );
            }).apply(compiler);
        });
    } else {
        let lastModulesCount = 0;
        let lastEntriesCount = 0;
        let moduleCount = modulesCount;
        let entriesCount = 1;
        let doneModules = 0;
        let doneEntries = 0;
        const activeModules = new Set();
        let lastActiveModule = "";

        const update = () => {
            const percentByModules =
                  doneModules / Math.max(lastModulesCount, moduleCount);
            const percentByEntries =
                  doneEntries / Math.max(lastEntriesCount, entriesCount);

            const items = [
                0.1 + Math.max(percentByModules, percentByEntries) * 0.6,
                "building"
            ];
            if (showEntries) {
                items.push(`${doneEntries}/${entriesCount} entries`);
            }
            if (showModules) {
                items.push(`${doneModules}/${moduleCount} modules`);
            }
            if (showActiveModules) {
                items.push(`${activeModules.size} active`);
                items.push(lastActiveModule);
            }
            handler(...items);
        };this

        const moduleAdd = module => {
            moduleCount++;
            if (showActiveModules) {
                const ident = module.identifier();
                if (ident) {
                    activeModules.add(ident);
                    lastActiveModule = ident;
                }
            }
            update();
        };

        const entryAdd = (entry, name) => {
            entriesCount++;
            update();
        };

        const moduleDone = module => {
            doneModules++;
            if (showActiveModules) {
                const ident = module.identifier();
                if (ident) {
                    activeModules.delete(ident);
                    if (lastActiveModule === ident) {
                        lastActiveModule = "";
                        for (const m of activeModules) {
                            lastActiveModule = m;
                        }
                    }
                }
            }
            update();
        };

        const entryDone = (entry, name) => {
            doneEntries++;
            update();
        };

        compiler.hooks.compilation.tap("ProgressPlugin", (compilation, ...params) => {
            if (compilation.compiler.isChild()) return;
            lastModulesCount = moduleCount;
            lastEntriesCount = entriesCount;
            moduleCount = entriesCount = 0;
            doneModules = doneEntries = 0;
            handler(0, "compiling");

            compilation.hooks.buildModule.tap("ProgressPlugin", moduleAdd);
            compilation.hooks.failedModule.tap("ProgressPlugin", moduleDone);
            compilation.hooks.succeedModule.tap("ProgressPlugin", moduleDone);

            compilation.hooks.addEntry.tap("ProgressPlugin", entryAdd);
            compilation.hooks.failedEntry.tap("ProgressPlugin", entryDone);
            compilation.hooks.succeedEntry.tap("ProgressPlugin", entryDone);

            //..拦截器部分省略
        compiler.hooks.done.tap("ProgressPlugin", () => {
            handler(1, "");
        });
    }
}
compiler

compiler就是上面讲的Compiler的实例,它拥有hooks属性(具体看Tapable和Hook分析,传送门 => 从vuecli3学习webpack记录(三)基类Tapable和Hook分析),通过调用插件的apply方法plugin.apply(compiler)传递进来
我们以简单的compiler.compilers为空时继续分析
我们平时看到的

就是上面的update方法实现的。
首先的实现肯定是已经完成的除以总共的,但是webpack是不会提供这个的接口给你的,但是它会把各个生命周期的钩子给你使用,这样我们就可以实现了。
那样我们就可以实现moduleAddmoduleDoneentryAddentryDone了。这里以稍微复杂一点的module系列为例。

compiler.hooks.compilation.tap("ProgressPlugin", compilation => {
    if (compilation.compiler.isChild()) return;
    lastModulesCount = moduleCount;
    lastEntriesCount = entriesCount;
    moduleCount = entriesCount = 0;
    doneModules = doneEntries = 0;
    handler(0, "compiling");

    compilation.hooks.buildModule.tap("ProgressPlugin", moduleAdd);
    compilation.hooks.failedModule.tap("ProgressPlugin", moduleDone);
    compilation.hooks.succeedModule.tap("ProgressPlugin", moduleDone);

    compilation.hooks.addEntry.tap("ProgressPlugin", entryAdd);
    compilation.hooks.failedEntry.tap("ProgressPlugin", entryDone);
    compilation.hooks.succeedEntry.tap("ProgressPlugin", entryDone);
}

compiler.hooks.compilation.tap就是实现对webpack编译过程的订阅,在编译时就会触发我们的回调了。这个回调函数其实是有两个参数的,分别是compilation, params,只是这里不需要第二个参数而已。
compilation又是什么呢?

compilation

compilation是Compilation类(同样继承自Tapable)的实例,并且它也拥有hooks属性。
这里我们订阅了buildModulefailedModulesucceedModule,其实webpack在编译过程中是不知道总共或完成多少个module的,进度插件时通过每触发一次buildModule就累计一次来实现的。当前正在活动的module是通过buildModule时其实就是在活动中了,添加在一个Set中,在failedModulesucceedModule时代表已经完成了,不应该还算在活动中了,这是看看当前完成的module是不是存在该Set中,如果存在删掉。
顺便提一下在ProcessPlugin里面还用到了拦截器intercept,


发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据