webpack中如何处理Loader加载器的规则

116 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情

关于Loader的逻辑RuleSet.js或者是NormalModule.js中,如果不太好找,可以在idea中直接全局搜索rule.test,如下:

image-20220811084031819.png

调用过程如下 webpack-module-rules-loader

webpack-module-rules-loader.png

在最后一个环节RuleSet下:

"use strict";
module.exports = class RuleSet {
    constructor(rules) {
        //askcomputer:创建一个干净的空对象,使用这种方法创建的对象,它不继承Object基类的原型链上的属性
        this.references = Object.create(null);
        this.rules = RuleSet.normalizeRules(rules, this.references, "ref-");
    }
​
    static normalizeRules(rules, refs, ident) {
        //askcomputer:判断rules规则是否是个数组
        if(Array.isArray(rules)) {
            //askcomputer:根据当前数组,数组中的每个元素执行响应的函数后,返回一个新的数组
            return rules.map((rule, idx) => {
                return RuleSet.normalizeRule(rule, refs, `${ident}-${idx}`);
            });
        } else if(rules) {
            return [RuleSet.normalizeRule(rules, refs, ident)];
        } else {
            return [];
        }
    }
​
    //askcomputer:规范规则
    static normalizeRule(rule, refs, ident) {
        //如果rule是字符串,则直接返回 {use: [{loader: rule}]
        if(typeof rule === "string")
            return {
                use: [{
                    loader: rule
                }]
            };
        //askcomputer:如果rule是null,则直接抛出异常
        if(!rule)
            throw new Error("Unexcepted null when object was expected as rule");
        //askcomputer:如果不是object,则直接抛出异常
        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;
        
        //askcomputer:这三个规则才是核心的问题,如果三个条件有一个为true,则执行 checkResourceSource
        if(rule.test || rule.include || rule.exclude) {
            //askcomputer:第一次加载这个规则的时候得到的结果是 resourceSource="test + include + exclude"
            checkResourceSource("test + include + exclude");
            condition = {
                test: rule.test,
                include: rule.include,
                exclude: rule.exclude
            };
            try {
                //askcomputer:规范化条件
                newRule.resource = RuleSet.normalizeCondition(condition);
            } catch(error) {
                throw new Error(RuleSet.buildErrorMessage(condition, error));
            }
        }
​
        if(rule.resource) {
            checkResourceSource("resource");
            try {
                newRule.resource = RuleSet.normalizeCondition(rule.resource);
            } catch(error) {
                throw new Error(RuleSet.buildErrorMessage(rule.resource, error));
            }
        }
​
        if(rule.resourceQuery) {
            try {
                newRule.resourceQuery = RuleSet.normalizeCondition(rule.resourceQuery);
            } catch(error) {
                throw new Error(RuleSet.buildErrorMessage(rule.resourceQuery, error));
            }
        }
​
        if(rule.compiler) {
            try {
                newRule.compiler = RuleSet.normalizeCondition(rule.compiler);
            } catch(error) {
                throw new Error(RuleSet.buildErrorMessage(rule.compiler, error));
            }
        }
​
        if(rule.issuer) {
            try {
                newRule.issuer = RuleSet.normalizeCondition(rule.issuer);
            } catch(error) {
                throw new Error(RuleSet.buildErrorMessage(rule.issuer, error));
            }
        }
​
        if(rule.loader && rule.loaders)
            throw new Error(RuleSet.buildErrorMessage(rule, new Error("Provided loader and loaders for rule (use only one of them)")));
 
        //askcomputer:loader部分--如果 loader 是 string,且 rule.options 和 rule.query 都不为空的情况下
        const loader = rule.loaders || rule.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)")));
        }
        
        //askcomputer:use
        if(rule.use) {
            checkUseSource("use");
            newRule.use = RuleSet.normalizeUse(rule.use, ident);
        }
        
        //askcomputer:rules
        if(rule.rules)
            newRule.rules = RuleSet.normalizeRules(rule.rules, refs, `${ident}-rules`);
        
        //askcomputer:oneOf
        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) {
            if(useSource && useSource !== newSource)
                throw new Error(RuleSet.buildErrorMessage(rule, new Error("Rule can only have one result source (provided " + newSource + " and " + useSource + ")")));
            useSource = newSource;
        }
​
        function checkResourceSource(newSource) {
            if(resourceSource && resourceSource !== newSource)
                throw new Error(RuleSet.buildErrorMessage(rule, new Error("Rule can only have one resource source (provided " + newSource + " and " + resourceSource + ")")));
            resourceSource = newSource;
        }
​
        if(Array.isArray(newRule.use)) {
            newRule.use.forEach((item) => {
                if(item.ident) {
                    refs[item.ident] = item.options;
                }
            });
        }
​
        return newRule;
    }
​
    static buildErrorMessage(condition, error) {
        const conditionAsText = JSON.stringify(condition, (key, value) => {
            return value === undefined ? "undefined" : value;
        }, 2);
        return error.message + " in " + conditionAsText;
    }
​
    //askcomputer:给 use的属性,新增了一个 ident 属性,并且规范了返回的几个属性,分别为 ident loader options
    static normalizeUse(use, ident) {
        if(Array.isArray(use)) {
            return use
                .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`))
                .reduce((arr, items) => arr.concat(items), []);
        }
        return [RuleSet.normalizeUseItem(use, ident)];
    }
    
    
    ...
​
    //askcomputer:规范化条件
    static normalizeCondition(condition) {
        //如果condition为空或者undefined,将会抛出异常
        if(!condition)
            throw new Error("Expected condition but got falsy value");
        if(typeof condition === "string") {
            return str => str.indexOf(condition) === 0;
        }
        //askcomputer:如果condition是函数,则直接返回
        if(typeof condition === "function") {
            return condition;
        }
        //askcomputer:如果condition是正则表达式,这个步骤可能稍微难理解一点。主要问题点应该是在 .test 和 .bind ,test用于判断在字符串中是否存在正则表达式对应的内容,如果有则返回true,如果没有返回false。而bind的目的是什么呢?对于给定的函数,创建一个与原始函数具有相同主体的绑定函数。绑定函数的this对象与指定对象相关联,并具有指定的初始参数。
        if(condition instanceof RegExp) {
            return condition.test.bind(condition); //askcomputer:这儿返回的是一个Function
        }
        //askcomputer:如果condition是数组
        if(Array.isArray(condition)) {
            const items = condition.map(c => RuleSet.normalizeCondition(c));
            return orMatcher(items);
        }
        //askcomputer:如果condition不是对象
        if(typeof condition !== "object")
            throw Error("Unexcepted " + typeof condition + " when condition was expected (" + condition + ")");
        //askcomputer:上述判断全是对于condition实参的验证内容
        const matchers = [];
        //askcomputer:遍历对象中的属性
        Object.keys(condition).forEach(key => {
            const value = condition[key];
            switch(key) {
                case "or":
                case "include":
                case "test":
                    if(value)
                        matchers.push(RuleSet.normalizeCondition(value));
                    break;
                case "and":
                    if(value) {
                        const items = value.map(c => RuleSet.normalizeCondition(c));
                        matchers.push(andMatcher(items));
                    }
                    break;
                case "not":
                case "exclude":
                    if(value) {
                        const matcher = RuleSet.normalizeCondition(value);
                        matchers.push(notMatcher(matcher));
                    }
                    break;
                default:
                    throw new Error("Unexcepted property " + key + " in condition");
            }
        });
        if(matchers.length === 0)
            throw new Error("Excepted condition but got " + condition);
        if(matchers.length === 1)
            return matchers[0];
        return andMatcher(matchers);
    }
​
    exec(data) {
        const result = [];
        this._run(data, {
            rules: this.rules
        }, result);
        return result;
    }
​
    _run(data, rule, result) {
        ...
    }
    ...
};
...

在上述代码块的第81行,得到的结果如下图:

image-20220811135757244.png

image-20220811135949677.png

其中关于resource的部分,是会让人难以理解的地方,如下位置: image-20220811141337699.png

这一些列返回的不是具体的值,而是Function。它在NormalModuleFactory.js执行的情况下会调用上图中定义的Function,如下:

image-20220811142500067.png

到目前为止,基本上全是判断和规则解析的过程,但是最终的目的应该是 如何将模块编译并且呈现渲染在浏览器中的,如何使用这些Loader进行转换的?否则这个流程就没有形成闭环。这一点很重要。

下一篇继续深入Loader的build过程。

小TIPS:在加载和编译过程会在终端输出模块编译的进度,它的逻辑在ProgressPlugin.js中,如下:

image-20220808182450197.png