.18-浅析webpack源码之compile流程-rules参数处理(1)

  Tips:写到这里,需要对当初的规则进行修改。在必要的地方,会在webpack.config.js中设置特殊的参数来跑源码,例如本例会使用module:{rules:[...]}来测试,基本上测试参数均取自于vue脚手架(太复杂的删掉)。

  下面两节的主要流程图如下:

  在进入compile方法后,迎面而来的就是这么一行代码:

const params = this.newCompilationParams();

  简单看一下方法,源码如下:

Compiler.prototype.newCompilationParams = () => {
    const params = {
        normalModuleFactory: this.createNormalModuleFactory(),
        contextModuleFactory: this.createContextModuleFactory(),
        compilationDependencies: []
    };
    return params;
}
// 工厂方法1
Compiler.prototype.createNormalModuleFactory = () => {
    const normalModuleFactory = new NormalModuleFactory(this.options.context, this.resolvers, this.options.module || {});
    this.applyPlugins("normal-module-factory", normalModuleFactory);
    return normalModuleFactory;
}
// 工厂方法2
Compiler.prototype.createContextModuleFactory = () => {
    const contextModuleFactory = new ContextModuleFactory(this.resolvers, this.inputFileSystem);
    this.applyPlugins("context-module-factory", contextModuleFactory);
    return contextModuleFactory;
}

  该方法生成了一个对象参数,而对象中的值又分别调用了各自的工厂方法。

  按顺序,首先来看NormalModuleFactory工厂方法,首先是构造函数:

class NormalModuleFactory extends Tapable {
    // context默认为命令执行路径
    // resolvers => {...}
    // options => options.module
    constructor(context, resolvers, options) {
        super();
        // 该参数在WebpackOptionsApply方法中被赋值
        this.resolvers = resolvers;
        // 处理module.rules或module.loaders
        // 两者意义一样
        this.ruleSet = new RuleSet(options.rules || options.loaders);
        // 谁会去设置这玩意???默认参数设置那会被置为true
        // 这里会生成一个返回布尔值的函数 可以简单理解为!!
        this.cachePredicate = typeof options.unsafeCache === "function" ? options.unsafeCache : Boolean.bind(null, options.unsafeCache);
        this.context = context || "";
        // 解析缓存
        this.parserCache = {};
        // 这两个方法超级长
        this.plugin("factory", () => (result, callback) => { /**/ });
        this.plugin("resolver", () => (data, callback) => { /**/ })
    }
}

  也是一个继承了Tapable框架的类,构造函数除去两个事件流注入不看,剩余的内容只有RuleSet那一块,也是本节的主要内容。

  从参数可以看出,这里是对module.rules处理的地方,本节中测试配置添加了rules方便测试:

const path = require('path');
// resolve => path.join(__dirname,'..',path)
module.exports = {
    entry: './input.js',
    output: {
        filename: 'output.js'
    },
    module: {
        rules: [
            {
                test: /.js$/,
                loader: 'babel-loader',
                include: [resolve('src'), resolve('test')]
            },
            {
                test: /.css/,
                loader: 'css-loader!style-loader'
            }
        ]
    }
};

  只针对配置中有或者常用且官网有解释的参数进行。

RuleSet

  构造函数如下:

module.exports = class RuleSet {
    constructor(rules) {
        // 空对象
        this.references = Object.create(null);
        // 格式化
        this.rules = RuleSet.normalizeRules(rules, this.references, "ref-");
    }
}

  生成了一个纯净对象并调用了本地静态方法进行格式化:

class RuleSet {
    constructor(rules) { /**/ }
        // rules => 传进来的配置数组
        // refs => 纯净对象
        // ident => 'ref-'
    static normalizeRules(rules, refs, ident) {
        // 分数组与非数组
        if (Array.isArray(rules)) {
            return rules.map((rule, idx) => {
                return RuleSet.normalizeRule(rule, refs, `${ident}-${idx}`);
            });
        } else if (rules) {
            return [RuleSet.normalizeRule(rules, refs, ident)];
        }
        // 未配置rules直接返回空数组 
        else {
            return [];
        }
    }
}

  这里也区分了数组参数与非数组参数,但是有个小bug

  看代码可以很容易猜到,数组与非数组的情况理论上是这样的:

// 数组
module.exports = {
    // ...
    module: {
        rules: [{ test: /.vue$/, loader: 'vue-loader' }, /*...*/ ]
    }
};
// 非数组
module.exports = {
    // ...
    module: {
        rules: { test: /.vue$/, loader: 'vue-loader' }
    }
};

  因为传非数组,会被包装成一个数组,所以这种情况属于单loader配置。

  但是,这样配置是会报错的,因为过不了validateSchema的验证,测试结果如图:

  这就很尴尬了,提供了非数组形式的处理方式,但是又不通过非数组的校验,所以这基本上是永远不会被执行的代码。

  管他的,估计源码太大,开发者已经顾不上了。

  下面正式进行格式化阶段,源码整理如下:

class RuleSet {
    constructor(rules) { /**/ };
    // ident => 'ref-${index}'
    static normalizeRule(rule, refs, ident) {
        // 传入字符串
        // 'css-loader' => use:[{loader:'css-loader'}]
        if (typeof rule === "string")
            return {
                use: [{
                    loader: rule
                }]
            };
        // 非法参数
        if (!rule)
            throw new Error("Unexcepted null when object was expected as rule");
        if (typeof rule !== "object")
            throw new Error("Unexcepted " + typeof rule + " when object was expected as rule (" + rule + ")");

        const newRule = {};
        let useSource;
        let resourceSource;
        let condition;
        // test
        if (rule.test || rule.include || rule.exclude) { /**/ }
        if (rule.resource) { /**/ }
        // 官网doc都懒得解释 估计是几个很弱智的参数
        if (rule.resourceQuery) { /**/ }
        if (rule.compiler) { /**/ }
        if (rule.issuer) { /**/ }
        // loader、loaders只能用一个
        if (rule.loader && rule.loaders)
            throw new Error(RuleSet.buildErrorMessage(rule, new Error("Provided loader and loaders for rule (use only one of them)")));

        const loader = rule.loaders || rule.loader;
        // 处理loader
        if (typeof loader === "string" && !rule.options && !rule.query) {
            checkUseSource("loader");
            newRule.use = RuleSet.normalizeUse(loader.split("!"), ident);
        }
        else if (typeof loader === "string" && (rule.options || rule.query)) {
            checkUseSource("loader + options/query");
            newRule.use = RuleSet.normalizeUse({
                loader: loader,
                options: rule.options,
                query: rule.query
            }, ident);
        } else if (loader && (rule.options || rule.query)) {
            throw new Error(RuleSet.buildErrorMessage(rule, new Error("options/query cannot be used with loaders (use options for each array item)")));
        } else if (loader) {
            checkUseSource("loaders");
            newRule.use = RuleSet.normalizeUse(loader, ident);
        } else if (rule.options || rule.query) {
            throw new Error(RuleSet.buildErrorMessage(rule, new Error("options/query provided without loader (use loader + options)")));
        }
        // 处理use
        if (rule.use) {
            checkUseSource("use");
            newRule.use = RuleSet.normalizeUse(rule.use, ident);
        }
        // 递归处理内部rules
        if (rule.rules)
            newRule.rules = RuleSet.normalizeRules(rule.rules, refs, `${ident}-rules`);
        // 不知道是啥
        if (rule.oneOf)
            newRule.oneOf = RuleSet.normalizeRules(rule.oneOf, refs, `${ident}-oneOf`);
        //
        const keys = Object.keys(rule).filter((key) => {
            return ["resource", "resourceQuery", "compiler", "test", "include", "exclude", "issuer", "loader", "options", "query", "loaders", "use", "rules", "oneOf"].indexOf(key) < 0;
        });
        keys.forEach((key) => {
            newRule[key] = rule[key];
        });

        function checkUseSource(newSource) { /**/ }

        function checkResourceSource(newSource) { /**/ }
        if (Array.isArray(newRule.use)) {
            newRule.use.forEach((item) => {
                if (item.ident) {
                    refs[item.ident] = item.options;
                }
            });
        }

        return newRule;
    }
}

  总体来看源码内容如下:

1、生成newRules对象保存转换后的rules

2、处理单字符串rule

3、处理test、include、exclude参数

4、处理resource、resourseQuery、compiler、issuer参数

5、处理loader、loaders、options、query参数

6、处理use参数

7、递归处理rules参数

8、处理oneOf参数

  内容比较多,还是分两节讲吧。

原文地址:https://www.cnblogs.com/QH-Jimmy/p/8109903.html