学标准化思想,还得看 webpack5 的 Rules 处理啊

106 阅读6分钟

一、前文回顾

本文接上文讨论的是解析行内 loader 和模块资源路径之后的事情——调用 continueCallback 的 callback;该 callback 主要做了以下工作:

  1. 处理行内 loader 的 ident 标识;
  2. 处理 matchResouce 及其 matchResource 语法相关的 userRequest、settings.type 等;
  3. 调用 nmf.ruleSet.exec 并传入相关数据获取当前 request 应该被应用的 loader;
  4. 遍历上一步获取到的 loader 集合,根据各个 loader 的 type 将各个 loader 分类,分别放到 pre/post/auto 三个数组中,分别对应 前置、后置、常规 loader,这里注意除了 type 本身的要求,还需要满足是否禁用其他 loader 的相关设置,即 noPrePostAutoLoader 等变量;

以上是大致的 callback 内容,其中有很多的细节内容我们还需要进一步探究,比如 ruleSet 这个东西哪里来的,又是如何工作的等.

今天这篇小作文我们将讨论 RuleSet 的相关工作原理和设计精华!

二、应用 loader 的计算

单纯讲这个过程是有点抽象的,结合 webpack 的用法这个过程就会变得清晰明了

在配置 webpack 时都会在 webpack.config.js.module.rules 中配置很多的 loader,比如对所有的 loader 使用 babel-loader:

// webpack.config.js

module.export = {
    module: {
        rules: [
            {
                test: /$\.js/,
                use: ['bable-loader']
            }
        ]
    }
}

但是 webpack 最终执行的 loader 并不止一个 babel-loader,这里面可能有我们是在 rules 声明的其他 loader,还有 webpack 内部的一些默认 loader,这个过程是个复杂的过程。现在我们需要学习这个处理思路。

在 webpack 有一个工具类 RuleSet 就是专门为了这个场景设计的,下面我们详细看看这个玩意儿

三、nmf.ruleSet 和 ruleSetCompiler

上文在说 callback 的时候提到过 nmf.ruleSet.exec 方法计算当前应该应用的常规 loader,现在我们看看这个 nmf.ruleSet 对象;

3.1 nmf.ruleSet 的初始化

这个对象是在 NMF 实例创建的过程中初始化的,代码如下:

class NormalModuleFactory extends ModuleFactory {
    constructor({ options, /* ... */ }) {
        
        // 这里完成 ruleSet 的初始化
        this.ruleSet = ruleSetCompiler.compile([
            {
                rules: options.defaultRules
            },
            {
                rules: options.rules
            }
        ]);
    }
}

nmf.ruleSet 的值为 ruleSetCompiler.compile 的返回值;

3.2 options.defaultRules

options 是创建 NMF 实例的过程中传入的,那这个过程中自然是在创建 Compilation 对象之前完成的。但是 defaultRules 不是我们在 webpack.config.js 中声明的,那么他是哪里来的呢?

这个东西是在 webpack 创建 compiler 对象时针对传入的 options 进行标准化处理的过程中添加的,既然是默认的 rules 自然是不需要外界声明的配置项:

  • webpack/lib/webpack.js

const { getNormalizedWebpackOptions } = require("./config/normalization");
const createCompiler = rawOptions => {  
    const options = getNormalizedWebpackOptions(rawOptions);
};

  • webpack/lib/config/normalization.js

const getNormalizedWebpackOptions = config => {  
    return {  
        amd: config.amd,  
        bail: config.bail,
        // ... 这里都是其他的 webpack 的配置项
        module: nestedConfig(config.module, module => ({  
            // .....
            defaultRules: optionalNestedArray(module.defaultRules, r => [...r]),   // 这里声明的 defaultRules
            rules: nestedArray(module.rules, r => [...r])  // 这是传入的
        })),
        // ...
    }
}

现在我们看看这里面有什么东西:

image.png

这里面的数据格式均遵循 webpack 的 Rule 格式!这其中一共有10项默认的 rule 分别对应 webpack 内部的实现的不同规则。

这其中包含了对 js/json/mjs/cjs 等 JS 和 JSON 格式的模块的处理规则,这里也是为什么说 webpack 本身只能识别 JS 和 JSON 的原因,根本还是 webpack 内置了这两种文件的默认 loader 而已。

3.3 options.rules

options.rules 就很简单了,就是我们在 webpack.config.js 中声明的 module.rules 选项了,也就是我们传入的 loader 相关配置。

image.png

这里是我们声明的 babel-loader 和使用 babel-loader 的 test 条件。

3.4 Rule

Rule 这个东西是个描述条件和应用 loader 的对象,webpack 官方文档这么描述: 每个 Rule 可以分为三部分 - 条件(condition),结果(result)和嵌套规则(nested rule)。

有关 webpack 的 rules 和 rule 相关选项请查询官方文档:webpack Rules

到这里可以揭晓了,webpack 内部是合并了 options.defaultRules 和 options.rules 的,使用这个合并后的集合匹配当前的 request,得出需要执行的常规 loader。

webpack 内部支持的 rule 配置超过 20 项,那这个时候如果每次都需要分门别类的处理效率非常低下,而且这要求每个开发的人都要知道每个 rule 配置项的工作原理,这也相当不切实际。那么怎么办呢?

webpack 内部把这些 rule 进行标准化,导出标准化之后的 rule 对象,这些 rule 细节被封装,然后导出一个返回真假值的方法 —— condition,当 condition 返回 true 则应用该 rule 对应的 loader。

这个工作就是 ruleSet 实现的,这种思路非常棒,在日常业务开发中也可以基于这种思路设计,比如有一套数据来自后端,后面的数据消费者并不关心数据细节,而是只需要根据具体条件按需取用某些结果集。

3.5 ruleSetCompiler

ruleSetCompiler 是一个在前面声明的常量,它是一个 RuleSetCompiler 的实例对象,代码如下:


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),
	new BasicMatcherRulePlugin("resource"),
	new BasicMatcherRulePlugin("resourceQuery"),
	new BasicMatcherRulePlugin("resourceFragment"),
	new BasicMatcherRulePlugin("realResource"),
	new BasicMatcherRulePlugin("issuer"),
	new BasicMatcherRulePlugin("compiler"),
	new BasicMatcherRulePlugin("issuerLayer"),
	new ObjectMatcherRulePlugin("assert", "assertions"),
	new ObjectMatcherRulePlugin("descriptionData"),
	new BasicEffectRulePlugin("type"),
	new BasicEffectRulePlugin("sideEffects"),
	new BasicEffectRulePlugin("parser"),
	new BasicEffectRulePlugin("resolve"),
	new BasicEffectRulePlugin("generator"),
	new BasicEffectRulePlugin("layer"),
	new UseEffectRulePlugin()
]);

结合代码可以发现,在实例化的时候传入了一大堆的插件实例,这其中包含四类:

  1. BasicMatcherRulePlugin:基础匹配规则插件
  2. ObjectMatcherRulePlugin:对象匹配器规则插件
  3. BasicEffectRulePlugin:基础效果规则插件
  4. UseEffectRulePlugin:使用效果规则插件(我是个二流翻译,信达雅一个没有,凑合着看吧😂,我一直不清楚 effect 到底该翻译成什么,vue3 里面一样,useEffect 巴拉巴拉的。。。。)

3.6 webpack 构建 plugin VS Rule plugin

要注意,这些插件和 webpack 的主流程用于编译的插件体系不一样,webpack 构建体系的插件apply 方法接受的都是 compiler 对象。

而这里这几个插件,是 webpack rule 体系的插件系统,工作原理与之前的 webpack 构建体系内插件原理相同:像预设的钩子订阅事件,但是 webpack rule 体系插件接受的对象是 ruleSetCompiler 实例对象!

四、总结

本文讲述了 webpack 内部与 loader 相关的重要概念 Rule 以及 rule 的具体生效基础 nmf.ruleSet,具体内容如下:

  1. nmf.ruleSet 的实例化过程,ruleSet 是 ruleSetCompiler.compile 方法的返回值;
  2. 介绍了 options.defaultRules 的出处,他的初始化实在 webpack 创建 compiler 实例的第一个步骤——格式化 options 选项对象时;
  3. 介绍了 options.rules,这个是 webpack.config.js 中声明的 loader 匹配条件和应用 loader;
  4. 介绍了 ruleSetCompier 常量,它是一个 RuleSetCompiler 实例,实例化过程中传入了一大堆插件实例;
  5. 最后介绍了 webpack 构建体系与 Rule 体系插件的异同点,这也是一个重点关注点!

最后呢,关于这几个插件和 RuleSetCompiler 的具体工作原理,我们后面一篇专门讨论!