webpack5 源码详解 - 初始化

222 阅读5分钟

Github原文

Webpack初始化

const webpack = require("webpack");
const config = require("./webpack.config");

const compiler = webpack(config);
compiler.run();

虽然大部分情况都在用cli或者dev-server跑webpack,它们能提供很多命令,接收参数,配置不同的npm script去跑不同的config等。但它们最终会跑以上代码的时候,开始进行打包的工作。当然,监听文件改动是用compiler.watch

webpack(config)

首先执行const compiler = webpack(config)

webpack.js

const webpack =  (
    (options, callback) => {
      //...
      const webpackOptions = (options);
            //构建compiler
      compiler = createCompiler(webpackOptions);
      //...	
      return { compiler };
    }
);

const createCompiler = rawOptions => {
    //将没处理过的options进行处理
    const options = getNormalizedWebpackOptions(rawOptions);

    //设置default值
    applyWebpackOptionsBaseDefaults(options);
    const compiler = new Compiler(options.context, options);

    //NodeEnvironmentPlugin会引入独立库(enhanced-resolve, NodeWatchFileSystem)来增强Node模块
    new NodeEnvironmentPlugin({
            infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);

    //注册外部plugin
    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);
    //...
    new WebpackOptionsApply().process(options, compiler);
    return compiler;
};

首先webpack会拿到options,并且调用createCompiler(options)生成compiler实例并返回。

getNormalizedWebpackOptions会先处理options,传进来的options并不是拿来就用,有许多配置需要处理。

//getNormalizedWebpackOptions.js
const getNormalizedWebpackOptions = config => {
    return {
        devServer: optionalNestedConfig(config.devServer, devServer => ({
                ...devServer
        })),
        entry: config.entry === undefined ? { main: {} }
                : typeof config.entry === "function" ?
                    (fn => () => Promise.resolve().then(fn)
                    .then(getNormalizedEntryStatic))(config.entry)
                    : getNormalizedEntryStatic(config.entry)
    };
	//...
};

applyWebpackOptionsBaseDefaultsapplyWebpackOptionsDefaults都是给没设置的基本配置加上默认值,先执行前面的是因为需要抛出options给下面的NodeEnvironmentPlugin使用

//如果没有该属性就设置工厂函数的返回值
const F = (obj, prop, factory) => {
    if (obj[prop] === undefined) {
            obj[prop] = factory();
    }
};

//如果没有该属性就进行设置
const D = (obj, prop, value) => {
    if (obj[prop] === undefined) {
            obj[prop] = value;
    }
};

const applyWebpackOptionsBaseDefaults = options => {
    //...
    F(infrastructureLogging, "stream", () => process.stderr);
    D(infrastructureLogging, "level", "info");
    D(infrastructureLogging, "debug", false);
    D(infrastructureLogging, "colors", tty);
    D(infrastructureLogging, "appendOnly", !tty);
};

const applyWebpackOptionsDefaults = options => {
    F(options, "context", () => process.cwd());
    F(options, "target", () => {
            return getDefaultTarget(options.context);
    });
    //...
    F(options, "devtool", () => (development ? "eval" : false));
    D(options, "watch", false);
    //...
}

处理完options之后就会实例化生成Compiler对象,这时候就可以往Compiler注入插件。它们会执行所有options.plugins里的apply方法,写过插件的人都知道,编写插件需要暴露apply函数,并且得到Compiler对象往compiler.hooks里注入钩子, 如果不清楚hook的用法,建议读我写的这篇文章

最后调用new WebpackOptionsApply().process(options, compiler)方法,为该有的配置去注册相应的插件。初始化Compiler的工作就完成了

//WebpackOptionsApply.js

//....
if (options.externals) {
    const ExternalsPlugin = require("./ExternalsPlugin");
    new ExternalsPlugin(options.externalsType, options.externals).apply(
            compiler
    );
}

if (options.optimization.usedExports) {
    const FlagDependencyUsagePlugin = require("./FlagDependencyUsagePlugin");
    new FlagDependencyUsagePlugin(
            options.optimization.usedExports === "global"
    ).apply(compiler);
}

//....

compiler.run()

run(callback) {
    //...
    const run = () => {
            //...
            this.compile(onCompiled);
        });
    };

    run()
}

//....

compile(callback) {
    //获取生成Compilation需要的参数
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
        if (err) return callback(err);

        this.hooks.compile.call(params);

        //生成compilation
        const compilation = this.newCompilation(params);

        const logger = compilation.getLogger("webpack.Compiler");

        logger.time("make hook");
        this.hooks.make.callAsync(compilation, err => {
                //...
        });
    });
}

run方法里会调用一些钩子与记录信息,在这里并不重要,主要在于this.compile(onCompiled),onCompiled是最终seal阶段之后的会执行的回调。

生成Compilation

compile函数首先会生成params给实例化Compilation作为参数

newCompilationParams() {
    const params = {
        normalModuleFactory: this.createNormalModuleFactory(),
        contextModuleFactory: this.createContextModuleFactory()
    };
    return params;
}

const params = this.newCompilationParams();

normalModuleFactory会生成normalModule,webpack里的模块就是normalModule对象。contextModuleFactory会生成contextModule,它是为了处理(require.context引用进来的模块。

createCompilation(params) {
    this._cleanupLastCompilation();
    //根据参数实例化Compilation
    return (this._lastCompilation = new Compilation(this, params));
}

newCompilation(params) {
    //实例化Compilation
    const compilation = this.createCompilation(params);
    compilation.name = this.name;
    compilation.records = this.records;
    //注册钩子
    this.hooks.thisCompilation.call(compilation, params);
    //注册钩子
    this.hooks.compilation.call(compilation, params);
    return compilation;
}

newCompilation会调用createCompilation实例化Compilation对象,并且调用钩子。

因为这时候compiler对象已经有了compilation和normalModule,所以可以传递给插件使用它们 , 或给它们的钩子注入函数实现相关功能。

在thisCompilation钩子里的插件有九个,compilation钩子甚至有四十几个,它们都是些内部插件。

thisCompilation.taps

Compilation.jpg

Compilation.taps

thisCompilation.jpg

ruleSetCompiler

在实例化normalModuleFactory的时候还会对rule进行处理,可以为之后处理模块的时候判断使用什么loader

//normalModuleFactory.js

const ruleSetCompiler = new RuleSetCompiler([
    new BasicMatcherRulePlugin("test", "resource"),
    new BasicMatcherRulePlugin("scheme"),
    new BasicMatcherRulePlugin("mimetype"),
    new BasicMatcherRulePlugin("dependency"),
    new BasicMatcherRulePlugin("include", "resource"),
    new BasicMatcherRulePlugin("exclude", "resource", true),
    //...
]);

class normalModuleFactory {
    construator() {
        //...
        this.ruleSet = ruleSetCompiler.compile([
            {
                    rules: options.defaultRules
            },
            {
                    rules: options.rules
            }
        ]);
        //...
    }
}

实例化ruleSetCompiler的时候会把自己作为参数给插件用。然后调用compile,将options.rules和options.defaultRules传入进去。defaultRules是在applyWebpackOptionsDefaults的时候生成的默认rules。

rules.jpg

//RuleSetCompiler.js

class RuleSetCompiler {
    constructor(plugins) {
        this.hooks = Object.freeze({
                //...
        });
        if (plugins) {
            for (const plugin of plugins) {
                plugin.apply(this);
            }
        }
    }
}

compile(ruleSet) {
    const refs = new Map();
    //编译rules
    const rules = this.compileRules("ruleSet", ruleSet, refs);

    //用于根据rule抛出对应的loader
    const execRule = (data, rule, effects) => {
            //..
    };

    return {
        references: refs,
        exec: data => {
            /** @type {Effect[]} */
            const effects = [];
            for (const rule of rules) {
                    execRule(data, rule, effects);
            }
            return effects;
        }
    };
}

compileRules(path, rules, refs) {
    return rules.map((rule, i) =>
        //递归options.rules和options.defaultRules
        this.compileRule(`${path}[${i}]`, rule, refs)
    );
}

compileRule(path, rule, refs) {
    //...
}

RuleSetCompiler.compile会调用compileRules("ruleSet", ruleSet, refs)拼凑path并递归进行处理。

第一次调用compileRules传进来的path为ruleSet,ruleSet是上面包含options.rules和options.defaultRules的数组 。

compileRule = (path, rule, refs)  => {
    const unhandledProperties = new Set(
            Object.keys(rule).filter(key => rule[key] !== undefined)
    );

    /** @type {CompiledRule} */
    const compiledRule = {
        conditions: [],
        effects: [],
        rules: undefined,
        oneOf: undefined
    };

    //判断是否含有rules的某些参数以加入到compiledRule里
    this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);

    //判断key是否包含rules
    if (unhandledProperties.has("rules")) {
        unhandledProperties.delete("rules");
        const rules = rule.rules;
        if (!Array.isArray(rules))
                throw this.error(path, rules, "Rule.rules must be an array of rules");
        compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs);
    }

    //判断key是否包含oneOf
    if (unhandledProperties.has("oneOf")) {
        unhandledProperties.delete("oneOf");
        const oneOf = rule.oneOf;
        if (!Array.isArray(oneOf))
                throw this.error(path, oneOf, "Rule.oneOf must be an array of rules");
        compiledRule.oneOf = this.compileRules(`${path}.oneOf`, oneOf, refs);
    }

    if (unhandledProperties.size > 0) {
        throw this.error(
                path,
                rule,
                `Properties ${Array.from(unhandledProperties).join(", ")} are unknown`
        );
    }

    return compiledRule;
}

compileRule会递归处理所有含有rules和oneOf的嵌套对象,比如传进来的path为rulSet[0],所以会取第一个对象为options.defaultRules。然后unhandledProperties会取出数组每个Object keys,options.defaultRules对象的key为'rules',所以满足unhandledProperties.has("rules")。会调用compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs)递归defaultRules数组

第二次递归path为rulSet[0].rules[0],然后会调用this.hooks.rule.call处理defaultRules里的每个规则。钩子会调用之前注册的BasicMatcherRulePlugin对rules的属性生成不同的conditions

class BasicMatcherRulePlugin {
    constructor(ruleProperty, dataProperty, invert) {
            this.ruleProperty = ruleProperty;
            this.dataProperty = dataProperty || ruleProperty;
            this.invert = invert || false;
    }
    apply(ruleSetCompiler) {
            ruleSetCompiler.hooks.rule.tap(
                "BasicMatcherRulePlugin",
                (path, rule, unhandledProperties, result) => {
                    if (unhandledProperties.has(this.ruleProperty)) {
                        unhandledProperties.delete(this.ruleProperty);
                        const value = rule[this.ruleProperty];
                        //生成Condition
                        const condition = ruleSetCompiler.compileCondition(
                            `${path}.${this.ruleProperty}`,
                            value
                        );
                        const fn = condition.fn;
                        //添加到compileRule里
                        result.conditions.push({
                            property: this.dataProperty,
                            matchWhenEmpty: this.invert
                                    ? !condition.matchWhenEmpty
                                    : condition.matchWhenEmpty,
                            fn: this.invert ? v => !fn(v) : fn
                        });
                    }
                }
        );
    }
}

比如rule为{ test: /\.js/ , use: babel-loader },插件new BasicMatcherRulePlugin("test", "resource")会处理所有包含test属性的rules,会生成如下:

[
    {
        conditions: [
            { property: "resource", matchWhenEmpty: false, fn:v => typeof v === "string" && condition.test(v) },
            { property: "resource", matchWhenEmpty: true, fn:v => !fn(v) }
        ],
        effects: [{ type: "use", value: { loader: "babel-loader" } }]
    }
];

condition就是/\.js/,对于之后调用exec解析js模块就会抛出babel-loader。处理完所有的rules后,RuleSetCompiler.compile会返回如下对象

{
    references: refs,
    //exec会对模块名执行符合的condition并抛出effects数组,effects包含对应的loader信息
    exec: data => {
        /** @type {Effect[]} */
        const effects = [];
        for (const rule of rules) {
            execRule(data, rule, effects);
        }
        return effects;
    }
};

之后只要执行RuleSetCompiler.exec()就能返回相对应的loader,使用方法如下

this.ruleSet.exec({
    resource: resourceDataForRules.path,		//资源的绝对路径
    realResource: resourceData.path,
    resourceQuery: resourceDataForRules.query,		//资源携带的query string
    resourceFragment: resourceDataForRules.fragment,	
    scheme,		//URL方案 ,列如,data,file
    assertions,
    mimetype: matchResourceData
            ? ""
            : resourceData.data.mimetype || "",   // mimetype
    dependency: dependencyType,			// 依赖类型
    descriptionData: matchResourceData		//	描述文件数据,比如package.json
            ? undefined
            : resourceData.data.descriptionFileData,
    issuer: contextInfo.issuer,						//发起请求的模块
    compiler: contextInfo.compiler,				//当前webpack的compiler
    issuerLayer: contextInfo.issuerLayer || ""
});

到这里,生成compilation的工作就做完了,继续Compiler的钩子流程,之后就是调用this.hooks.make.callAsync方法了,开始从入口构建模块。之后会有很多async hook的代码,因为是异步的原因所以会有callback hell问题,阅读起来特别恶心,而且因为async hook里可以是setTimeout,源码实现也并没有返回promise,所以也不能使用async await解决回调问题

总结

以上就是一些初始化的代码,处理options,rules,注册插件,实例化normalModule,compilation对象,调用钩子传递对象给插件使用等。所有的工作做完了,会调用make hook开始后面的构建环节。