乐趣区

关于前端:webpack5-源码详解-初始化

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 {
        cache: optionalNestedConfig(config.cache, cache => {if (cache === false) return false;
            if (cache === true) {
                return {
                    type: "memory",
                    maxGenerations: undefined
                };
            }
            switch (cache.type) {
                case "filesystem":
                    return {//....};
                case undefined:
                case "memory":
                    return {
                        type: "memory",
                        maxGenerations: cache.maxGenerations
                    };
                default:
                    throw new Error(`Not implemented cache.type ${cache.type}`);
            }
        }),
        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.taps

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。

//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 开始前面的构建环节。

退出移动版