一、前文回顾
上文就讨论一个生成 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 生效的原理!他们的生效依托于一个工具类—— RuleSetCompiler!
二、RuleSetCompiler 类
前面两篇其实一直在讨论 RuleSetCompiler 这个类的参数—— BasicMatcherRulePlugin、ObjectMatcherRulePlugin、BasicEffectRulePlugin、UseEffectRulePlugin 的实例!
现在我们来看看 RuleSetCompiler 这个类!
class RuleSetCompiler {
constructor () {}
compile () {}
compileRules () {}
compileRule () {}
compileCondition () {}
combineConditionsOr () {}
combineConditionsAnd () {}
error () {}
}
接下来我们将要研究包括构造函数在内的以上这 8 个方法:
2.1 constructor
class RuleSetCompiler {
constructor(plugins) {
// 1.
this.hooks = Object.freeze({
rule: new SyncHook([
"path",
"rule",
"unhandledProperties",
"compiledRule",
"references"
])
});
// 2.
if (plugins) {
for (const plugin of plugins) {
plugin.apply(this);
}
}
}
}
该类型在 NMF 中初始化,接收的一堆插件实例,这些插件实例都是用于处理各种 Rule 的,比如 rule.loader、rule.use 等。
构造函数做的事情很简单:
- 初始化了 RuleSetCompiler 实例的 hooks 对象,这个里面只有一个 hook —— rule,同步 hook;
- 如果初始化的时候传递了 plugins 数组,则遍历这个 plugins 数组,逐个调用各个插件的 apply 方法,完成插件初始化。这些插件都会向 hooks.rule 添加订阅事件,这些时间就是注册的各个 rule 的处理;
2.2 compile
该方法接受 ruleSet 对象,返回一个带有 exec 属性的对象:
class RuleSetCompiler {
compile(ruleSet) {
// 1.
const refs = new Map();
// 2.
const rules = this.compileRules("ruleSet", ruleSet, refs);
// 3.
const execRule = (data, rule, effects) => { /* .... */ };
// 4.
return {
references: refs,
exec: data => {
const effects = [];
for (const rule of rules) {
execRule(data, rule, effects);
}
return effects;
}
};
}
}
2.2.1 参数 ruleSet
先来看看 ruleSet 传入了什么:
class NormalModuleFactory {
constructor () {
// ....
this.ruleSet = ruleSetCompiler.compile([
{
rules: options.defaultRules
},
{
rules: options.rules
}
]);
// ...
}
}
这个 compile 被调用的过程发生在 NMF 实例化的过程中,从上面的代码我们可以清晰的看出所谓 ruleSet 就是个数组,数组项均为 带有 rules 属性的对象,结合调用我们看看里面都有什么!

结合前面我们已经讨论过的 rules,从图中我们可以看出第一项是是 webpack 内部提供的 defaultRules,第二项则是我们在 webpack.config.js 中声明的 module.rules 配置项。
2.2.2 整体逻辑
整个过程分为 4 个步骤:
- 声明一个 refs 常量,值为 Map 实例对象;
- 调用 this.compileRules 方法编译规则,返回值保存在 rules 上;
- 声明 execRule 内部方法,该方法是实现 exec 功能的的核心方法,下面重点展开讲;
2.2.3 execRule 方法
该方法是计算某一个 request 或者说一个 path 应该应用那些规则以及这些规则背后对应 loader 的核心实现,从上面的代码可以看出 nmf.ruleSet.exec 内部就是执行的这个方法。
下面我们看看这个方法的核心实现:
const execRule = (data, rule, effects) => {
// 1.
for (const condition of rule.conditions) {
const p = condition.property;
// 2.
if (Array.isArray(p)) {
// 3
let current = data;
// 4.
for (const subProperty of p) {
if (
current &&
typeof current === "object" &&
Object.prototype.hasOwnProperty.call(current, subProperty)
) {
current = current[subProperty];
} else {
current = undefined;
break;
}
}
// 5.
if (current !== undefined) {
if (!condition.fn(current)) return false;
continue;
}
} else if (p in data) {
// 6.
const value = data[p];
if (value !== undefined) {
if (!condition.fn(value)) return false;
continue;
}
}
// 7.
if (!condition.matchWhenEmpty) {
return false;
}
}
// 8.
for (const effect of rule.effects) {
if (typeof effect === "function") {
const returnedEffects = effect(data);
for (const effect of returnedEffects) {
effects.push(effect);
}
} else {
effects.push(effect);
}
}
// 9.
if (rule.rules) {
for (const childRule of rule.rules) {
execRule(data, childRule, effects);
}
}
// 10.
if (rule.oneOf) {
for (const childRule of rule.oneOf) {
if (execRule(data, childRule, effects)) {
break;
}
}
}
// 11.
return true;
};
整个过程分为 16 个步骤:
- 先处理 condition, 遍历 rule.conditions,从每个 condition 中取出 property 属性赋值给常量 p;
- 判断 p 如果是数组,则进行后续操作;
- 缓存当前 data 到 current;
- 进一步遍历 p,尝试检测 p 中是否包含当前的规则属性,如果包含则则需要进一步验证,如果有一个不包含则说明不满足条件终止对 p 的遍历,同时置空 current 变量;
- 这里说明 current 是个对象,此时调用 condition.fn 这个验证函数进行验证,如果验证没通过则返回 false,说明没有匹配到!
- 走到这里说明 data.property 不是数组并且 data 中包含 p 属性,最常见的就是字符串,代码执行走的就是这个分支,此时判断 data 中是否包含这个属性,如果包含则取出对应的值,传给验证函数 condition.fn,如果验证未通过,则返回 false;
- 这里说明针对 property 的验证均未匹配到,此时如果 condition 条件中没有允许空之,即没有 matchWhenEmpty 属性,则此时说明为匹配,返回 false;
- 从这里开始处理 rule.effects,effect 的处理比较粗暴,直接遍历 rule.effects,判断各个 effect 是否为函数数据类型,如果是函数,则传入 data 尝试调用 effect 并获取其返回值,然后再遍历该返回值,把返回值加入到 effects 中;如果 effect 不是函数数据类型,则直接加入到 effects 中;
- 这里开始处理 rule.rules 嵌套 rules,这里就是一个简单的递归调用,为各个子规则调用 execRule 方法;
- 这里处理 rule.oneOf,oneOf 和上面的嵌套 rules 不同,oneOf 是有一个命中即命中,所以遍历时一旦命中则需要 break 终止对 oneOf 的进一步处理;
- 最后则是 execRule 的返回结果 true,结合前面的 rules 和 oneOf 可知这个方法 返回 boolean 是为控制递归过程中的执行流;
2.3 compileRules 和 compileRule
class RuleSetCompiler {
compileRules(path, rules, refs) {
return rules.map((rule, i) =>
this.compileRule(`${path}[${i}]`, rule, refs)
);
}
compileRule(path, rule, refs) {
// 1.
const unhandledProperties = new Set(
Object.keys(rule).filter(key => rule[key] !== undefined)
);
// 2.
const compiledRule = {
conditions: [],
effects: [],
rules: undefined,
oneOf: undefined
};
// 3.
this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);
// 4.
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);
}
// 5.
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);
}
// 6.
if (unhandledProperties.size > 0) {
throw this.error(
path,
rule,
`Properties ${Array.from(unhandledProperties).join(", ")} are unknown`
);
}
// 7.
return compiledRule;
}
}
1. ruleSetCompiler.compileRules
ruleSetCompiler.compileRules 方法内部很简单,遍历 rules 并且为每一个 rule 调用 ruleSetCompiler.compileRule 方法编译 rule,实现 rule 的标准化。
2. ruleSetCompiler.compileRule
而 ruleSetCompiler.compileRule 方法内部则是做了以下主要工作:
- 声明 unhandledProperties 方法,该方法用于获取待处理的属性;
- 声明标准的编译后的 rule 结果对象 compiledRule,其中包含 conditions、effects、rules、oneOf 属性;
- 触发 this.hooks.rule 钩子,前面说过 BasicMatcherRulePlugin 等插件注册的过程中会向 hooks.rules 钩子上注册事件,用以添加 rule 和 effects;
- 处理嵌套的 rules,如果当前的 rule 还有 rules 属性即嵌套 rule,此时则递归调用 this.compileRules 进行处理;
- 处理 oneOf,递归调用 this.compileRules 对 rule.oneOf 进行处理;
- 针对 webpack 未知属性抛出警告;
- 返回经过处理的 compileRule 对象;
2.5 condition 函数编译
compileCondition & combineConditionsOr & combineConditionsAnd,这三个方法都是构建复合的逻辑条件函数用的,所谓条件函数实际上就是处理 rule.test 的,其中:
- compileCondition:根据条件的不同,返回不同的函数,比如 condition 是正则,则返回正则的 test 方法调用;
- combineConditionsOr:构造能产生”逻辑或“ 的函数;
- combineConditionsAnd:构造能产生 ”逻辑与“ 的函数;
三、总结
本文详细介绍了 RuleSetCompiler 类型的工作原理,回顾整个调用过程:
- 首先 NMF 实例创建了 RuleSetCompiler 的实例;
- 接着调用了 ruleSetCompiler.compileRules 编译 rules,得到带有 exec 方法的 nmf.ruleSet 对象;
- 在有了模块路径后调用上一步得到的 ruleSet.exec 方法获取当前模块路径需要应用的 loader;