怎么写个 test: /.js$/ babel-loader 就好使了呢?

104 阅读5分钟

一、前文回顾

上文介绍了和 rule 相关的前三个插件—— BasicMatcherRulePlugin、ObjectMatcherRulePlugin、BasicEffectRulePlugin,下面我们看看他们三个的作用和生效的原理:

  1. BasicMatcherRulePlugin:注册包括 scheme、mimetype 在内的 basic rule,其内部是被 ruleSetCompiler.compileCondition 方法编译成 condition.fn 函数并加入到 result.conditions 中;
  2. ObjectMatcherRulePlugin:处理对象 rule,其内部是遍历这个对象,把每个属性都编译成一个 conditon.fn 函数加入到 result.conditions;
  3. BasicEffectRulePlugin:处理 effect rule,这个插件和前两个有所不同,它是直接把 ruleProperty 对应的的 value 加入到 result.effects 中;

webpack 中一共包含了 4 个插件,这里我们讨论了三个,这一篇我们讨论最后一个 UseEffectPlugin!

二、UseEffectRulePlugin

UseEffectRulePlugin 插件是最后一个,也是四个插件中最长的,它的作用从篇幅上讲就是举足轻重的! 该插件不是 使用 effect 插件,而是 use 这个属性的 effct 插件,说白了就是 rule.use 的效果插件!这个名字起的和前面的没啥关系😂

整个插件通篇都在处理 rule.use 配置,所以叫做 useEffectRulePlguin,就是相当于“海参炒面”,里面真的是海参 + 面条,而不是厨师“海参”

2.1 apply 方法


class UseEffectRulePlugin {
    apply(ruleSetCompiler) {
        // 1.
        ruleSetCompiler.hooks.rule.tap(
            "UseEffectRulePlugin",
            (path, rule, unhandledProperties, result, references) => {
                // 2.
                const conflictWith = (property, correctProperty) => { /* ..... */ };
                // 3.
                if (unhandledProperties.has("use")) {
                    // 4.
                    unhandledProperties.delete("use");
                    unhandledProperties.delete("enforce");
                    
                    // 5.
                    conflictWith("loader", "use");
                    conflictWith("options", "use");

                    // 6.
                    const use = rule.use;
                    const enforce = rule.enforce;
                    
                    // 7.
                    const type = enforce ? 'use-'+ enforce : "use";

                    // 8.
                    const useToEffect = (path, defaultIdent, item) => { /*... */ };

                    // 9.
                    const useToEffectRaw = (path, defaultIdent, item) => { /* ... */ };

                    // 10.
                    const useToEffectsWithoutIdent = (path, items) => { /* .... */ };
                    
                    // 11.
                    const useToEffects = (path, items) => {  /* .... */ };

                     // 12.
                    if (typeof use === "function") {
                        result.effects.push(data =>
                            useToEffectsWithoutIdent(path + '.use', use(data))
                        );
                    } else {
                        for (const effect of useToEffects(path + '.use', use)) {
                            result.effects.push(effect);
                        }
                    }
                }
               
                if (unhandledProperties.has("loader")) {
                    // 13.
                    unhandledProperties.delete("loader");
                    unhandledProperties.delete("options");
                    unhandledProperties.delete("enforce");
                    
                    // 14.
                    const loader = rule.loader;
                    const options = rule.options;
                    const enforce = rule.enforce;
                    
                    // 15.
                    if (loader.includes("!")) {
                       // loader 中不能再用行内loader
                    }
                    
                    // 16.
                    if (loader.includes("?")) {
                        // loader 中不可以用 ? 传参
                    }

                    // 17.
                    if (typeof options === "string") {
                       // 提示 options 不能用 string
                    }

                    // 18.
                    const ident =
                            options && typeof options === "object" ? path : undefined;
                    references.set(ident, options);
                    result.effects.push({
                        type: enforce ? 'use-' + enforce : "use",
                        value: {
                            loader,
                            options,
                            ident
                        }
                    });
                }
            }
        );
    }
}

  1. 下面我们整体感受一下各个步骤都做了什么工作:
  2. 订阅 ruleSetCompiler.hooks.rule 钩子,以下为订阅钩子的逻辑;
  3. 声明 conflictWith 方法,该方法输出属性冲突的错误;
  4. 判断 unhandledProperties 中是否存在 use 属性,如果有则进行以后的操作;
  5. 删除 use 和 enforce 属性;
  6. 处理 loader 和 use 两个属性的冲突,接着处理另一对冲突的属性 options 和 use 属性,之所处理冲突是因为这两对属性不能同时配置,彼此是互斥的;
  7. 获取 rule.use 和 rule.enforce 属性,分别保存到 use 和 enforce 常量;
  8. 根据是否存咋 rule.enforce 生成 type 类型,如果有就是 "use-pre" 或者 "use-post" 这种强制某种规则的前提下前置或者后置使用该 loader,如果不配置 enforce 则 type 就是 "use";
  9. 声明 useToEffect 方法,该方法作用及细节下面详述;
  10. 声明 useToEffectRaw 方法,该方法原理下面详述;
  11. 声明 useToEffectsWithoutIdent 方法,原理下面详述;
  12. 声明 useToEffects 方法,该方法原理下面详述;
  13. 判断 use 类型是否为 "function" 类型,如果是则向 result.effects 中 push 一个函数,该函数接收 参数 data,返回 useToEffectsWithoutIdent(${path}.use, use(data)) 调用,其中 use 会接收 data 并把返回值传递给 useToEffectsWithoutIdent。这里这个处理就是我们在 webpack.config.js 中可以为 rule.use 配置 一个函数的原因;若不是函数数据类型则遍历 useToEffects(path + '.use', use) 的返回值,把这个返回值的每一项添加到 result.effects 中;
  14. 获取 rule.loader/options/enforce 三个属性,缓存到 loader、options、enforce 三个常量;
  15. 判断 loader 中是否包含 "!",如果有则进行报错提示?为啥有这个限制,这是因为 webpack 不允许在配置的 loader 中再内联其他loader,比如你这么配置:{ loader: 'style-loader!css-loader!some-other-loader' },这种语法将会被 webpack 认定非法!
  16. 判断 loader 中是否包含 "?" 这个字符串,如果有则进行报错提示?之所以有这个限制,这是因为 webpack 不允许在配置的 loader 中加入查询字符串,例如 { loader: 'style-loader?root=xyz&test=abc' } 这种语法将会被 webpack 认定为非法,正确做法就是通过 options 进行数据传递;
  17. 判定 options 是否为 字符串类型,如果使用字符串,同样会被 webpack 认定为非法类型;
  18. 计算 ident 标识;
  19. 缓存 ident 和 options;
  20. 最后将经过表转化处理的对象:{ type, value } ,推入到 result.effects 当中;

2.2 apply 中的 conflictWith 方法

这个方法用于抛出错误提示,提示的信息主要为两个属性冲突:

const conflictWith = (property, correctProperty) => {
    if (unhandledProperties.has(property)) {
        throw ruleSetCompiler.error(
            `{path}.${property}`,
            rule[property],
            `A Rule must not have a '${property}' property when it has a '${correctProperty}' property`
        );
    }
};

方法内容很简单,这里面就是一个报错提示;但是需要注意的是这些冲突的属性:

  1. "loader" 和 "use",不可以在同一个 rule 中同时配置;
  2. "options" 和 "use",不可以在同一个 rule 中配置;

2.3 useToEffect

这个方法是干嘛用的呢?顾名思义,use 变成 effect,下面我们看看这个方法:

const useToEffect = (path, defaultIdent, item) => {
    // 1.
    if (typeof item === "function") {
        // 2.
        return data => useToEffectsWithoutIdent(path, item(data));
    } else {
        // 3.
        return useToEffectRaw(path, defaultIdent, item);
    }
};
  1. 判断 item 是否为函数类型;
  2. 如果是则返回一个函数,这个函数接收data,返回 useToEffectsWithoutIdent 方法的返回值;
  3. 如不是函数数据类型,则返回 useToEffectRaw 方法的返回值;

useToEffectsWithoutIdent 方法和 useToEffectRaw 方法都是根据给定的 effect 构造出标准的 effect 对象。下面我们一个逐个看看,内部的构造逻辑!

2.4 useToEffectRaw

该方法用于创建标准的 effect 对象,现在我们看看他的逻辑:

const useToEffectRaw = (path, defaultIdent, item) => {
    // 1.
    if (typeof item === "string") {
        return {
            type,
            value: {
                loader: item,
                options: undefined,
                ident: undefined
            }
        };
    } else {
        // 2.
        const loader = item.loader;
        const options = item.options;
        let ident = item.ident;
        
        // 3.
        if (options && typeof options === "object") {
            if (!ident) ident = defaultIdent;
            references.set(ident, options);
        }
        
        // 4.
        if (typeof options === "string") {
            util.deprecate(
                () => {},
                `Using a string as loader options is deprecated (${path}.options)`,
                "DEP_WEBPACK_RULE_LOADER_OPTIONS_STRING"
            )();
        }
        
        // 5.
        return {
            type: enforce ? `use-${enforce}` : "use",
            value: {
                loader,
                options,
                ident
            }
        };
    }
};

  1. 判断 item 是否为字符串,如果是则返回 effect 对象:{ type, value: { loader: item } };
  2. 如果不是字符串,说明 item 是个对象,这个时候从 item 上获取 loader、options、ident 三个属性值;
  3. 判断 options 是否为对象,如果是则换成 ident 和 options;
  4. 判断 options 是否是字符串,如果是则抛出一个废弃警告,因为 options 以及不可以穿字符串了;
  5. 最后返回标准 effect 对象,其中 type 需要根据是否有 enforce 配置,如有则是 use-pre 或者 use-post,强制当前 loader 作为前置或者后置 loader 执行;

2.5 useToEffects

这个方法是吧 use 变成多个标准 effect 对象,所以是复数 effects;其实和前面的差不多,只不过需要遍历:

const useToEffects = (path, items) => {
    // 1.
    if (Array.isArray(items)) {
        return items.map((item, idx) => {
            const subPath = `${path}[${idx}]`;
            return useToEffect(subPath, subPath, item);
        });
    }
    
    // 2.
    return [useToEffect(path, path, items)];
};
  1. 判断传入的 items 是否是数组,如果是数组,则把数组中的每一项调用 useToEffect 变成标准的 effect 对象;
  2. 这里说明 items 不是数组,这个时候就返回一个只有一项的包装数组!

三、总结

本文就讨论一个生成 rule 的插件 —— UseEffectRulePlugin,这个插件是处理 rule.use 配置的,内部通过一系列的操作吧 use 数组变成标准的 effect 对象。

effect 对象,这里是 webpack 源码级别的对象,其中包含 type 和 value 两个属性:{ type, value },这两个属性的意义如下:

  • type:标识当前的 loader 它被应用的时机,即前置、后置、还是普通 loader;
  • value:这里面包含了 loader 所有的信息,包括 loader 本身、传递给 loader 的 options 对象以及当前 loader 的标识 ident

好了,到这里我们说完了 webpack 内部用于标准化 loader 的四个插件,四个插件分别对应不同的规则,最后这个则是处理 use 这个终极 effect 的。

下文我们将会解密,这些 effect 生效的原来!