一、前文总结
上文讲述了 webpack 内部与 loader 相关的重要概念 Rule 以及 rule 的具体生效基础 ------ nmf.ruleSet,具体内容如下:
- nmf.ruleSet 的实例化过程,ruleSet 是 ruleSetCompiler.compile 方法的返回值;
- 介绍了 options.defaultRules 的出处,他的初始化实在 webpack 创建 compiler 实例的第一个步骤------格式化 options 选项对象时;
- 介绍了 options.rules,这个是 webpack.config.js 中声明的 loader 匹配条件和应用 loader;
- 介绍了 ruleSetCompier 常量,它是一个 RuleSetCompiler 实例,实例化过程中传入了一大堆插件实例;
- 最后介绍了 webpack 构建体系与 Rule 体系插件的异同点,这也是一个重点关注点!
关于这几个插件和 RuleSetCompiler 的具体工作原理是我们今天这一篇要研究的重点!
二、rulePlugins
这个主题我们看看 ruleSetCompiler 实例化用到的 4 类插件具体工作原理!
2.1 BasicMatcherRulePlugin
基础匹配器插件,从注册的属性看,这个插件用的是最多的,我们看看它都注册了哪些 rule:
- new BasicMatcherRulePlugin("test", "resource"):Rule.resouce:匹配资源路径,webpack 文档:Rule.resource
- new BasicMatcherRulePlugin("scheme"):Rule.scheme,匹配声明的 scheme,webpack 文档 Rule.scheme
- new BasicMatcherRulePlugin("mimetype"):Rule.mimetype,匹配 data 对应 uri 的 mimetype Rule.mimetype
- new BasicMatcherRulePlugin("dependency"):Rule.dependency,这个文档暂时没有声明作用;
- new BasicMatcherRulePlugin("include", "resource"),Rule.include 置顶必须包含的 resource, Rule.include
- new BasicMatcherRulePlugin("exclude", "resource", true),Reclude.exclude Rule.exclude 指定必不包含的 resource,文档传送门
- new BasicMatcherRulePlugin("resource"):同上 resource
- new BasicMatcherRulePlugin("resourceQuery"):匹配指定的资源路径查询字符串,Rule.resourceQuery 文档传送门
- new BasicMatcherRulePlugin("resourceFragment"):文档未声明,据之前讲 resolver 的经验看,fragment 指的是 resource 后的 #哈希,比如 '/a/b/c.js?some=test#thisFragment',这么多年没见过😂
- new BasicMatcherRulePlugin("realResource"):realResource 一般与 resource 相等,但是当被 !=! matchResource 语法覆写后才不相等;
- new BasicMatcherRulePlugin("issuer"):Rule.issuer,匹配 request 的发起者;Rule.issuer 文档
- new BasicMatcherRulePlugin("compiler"):暂未查询到文档;
- new BasicMatcherRulePlugin("issuerLayer"):允许按 issuer 的 layer 进行过滤/匹配,Rule.issuerLayer 文档
2.1.1 构造函数
先来看看构造函数的参数:
- ruleProperty, 规则属性,用于构造 condition.fn,匹配函数的基准属性;
- dataProperty, 数据属性,用于构造 condition.property,不传时等于 ruleProperty;
- invert, 取反标识符,是否对结果进行取反
js
class BasicMatcherRulePlugin {
constructor(ruleProperty, dataProperty, invert) {
this.ruleProperty = ruleProperty;
this.dataProperty = dataProperty || ruleProperty;
this.invert = invert || false;
}
apply(ruleSetCompiler) {
}
}
2.1.2 apply 方法
js
class BasicMatcherRulePlugin {
constructor(ruleProperty, dataProperty, invert) {}
apply(ruleSetCompiler) {
// 1.
ruleSetCompiler.hooks.rule.tap(
"BasicMatcherRulePlugin",
(path, rule, unhandledProperties, result) => {
// 2.
if (unhandledProperties.has(this.ruleProperty)) {
// 3.
unhandledProperties.delete(this.ruleProperty);
// 4.
const value = rule[this.ruleProperty];
// 5.
const condition = ruleSetCompiler.compileCondition(
`${path}.${this.ruleProperty}`,
value
);
// 6.
const fn = condition.fn;
result.conditions.push({
property: this.dataProperty,
matchWhenEmpty: this.invert
? !condition.matchWhenEmpty
: condition.matchWhenEmpty,
fn: this.invert ? v => !fn(v) : fn
});
}
}
);
}
}
- 订阅 ruleSetCompiler.hooks.rule 钩子,以下为定于的具体逻辑;
- 判断当前 ruleProperty 是否已经处理过(unhandledRuleProperties.has(xxx)),如果未处理再执行后续逻辑;
- 走到这里说明 ruleProperty 未经处理,此时先从 unhandledRuleProperties 中剔除当前 ruleProperty;
- 从 rule 中取用当前 ruleProperty 中取出对应的值;
- 调用 ruleSetCompiler.compieCondition 把 ruleProperty,path,value 编译成一个带有匹配函数的 fn 的对象 condition;
- 向 result 结果集合中加入当前 ruleProperty 构造的标准 condition 对象;
这里来一个例子,以 mimetype 为例,看下 condition.fn:

2.2 ObjectMatcherRulePlugin
该插件用于处理对象类型的 rule 匹配,和上面的基础规则类似,唯一不同的就是需要将传入的对象进行展开;
2.2.1 构造函数
js
class ObjectMatcherRulePlugin {
constructor(ruleProperty, dataProperty) {
this.ruleProperty = ruleProperty;
this.dataProperty = dataProperty || ruleProperty;
}
apply(ruleSetCompiler) { }
}
构造函数功能很简单,保存 ruleProperty 和 dataProperty,当并传入 dataProperty 时,用 ruleProperty 兜底;
2.2.2 apply 方法
apply 方法是实现的核心,我们看看它内部实现的能力:
js
class ObjectMatcherRulePlugin {
constructor(ruleProperty, dataProperty) {}
apply(ruleSetCompiler) {
// 1.
const { ruleProperty, dataProperty } = this;
// 2.
ruleSetCompiler.hooks.rule.tap(
"ObjectMatcherRulePlugin",
(path, rule, unhandledProperties, result) => {
// 3.
if (unhandledProperties.has(ruleProperty)) {
// 4.
unhandledProperties.delete(ruleProperty);
// 5.
const value = rule[ruleProperty];
for (const property of Object.keys(value)) {
// 6.
const nestedDataProperties = property.split(".");
// 7.
const condition = ruleSetCompiler.compileCondition(
`${path}.${ruleProperty}.${property}`,
value[property]
);
// 8.
result.conditions.push({
property: [dataProperty, ...nestedDataProperties],
matchWhenEmpty: condition.matchWhenEmpty,
fn: condition.fn
});
}
}
}
);
}
}
- 获取构造函数执行时保存的 uleProperty, dataProperty 属性;
- 订阅 ruleSetCompiler.hooks.rule 钩子,以下为订阅函数的处理逻辑;
- 判断 unhandledProperties 是否包含 当前 ruleProperty,若不包含再继续后续逻辑;
- 从 unhandledProperties 中移除当前 ruleProperty;
- 从 rule 中获取 ruleProperty 对应的值 value 并遍历该 value 对象(for of Object.keys(value));
- 将嵌套属性进行分拆;
- 调用 ruleSetCompiler.compileCondition 为当前的 property 构造 condition.fn 函数;
- 构造标准的 condition 对象推入最终结果集合 result 中;
对比 basic 的插件,这个算是一个加强版,他会把这个对象展开,为每个属性都构造一个 condition,相当于是一个拍平的动作。
2.3 BasicEffectRulePlugin
我把这个插件翻译成 基础效果规则插件,这个 effect 真的不知道咋翻译。名字而已不是重点,我们看看它和前面的 BasicMathcerRulePlugin 有啥区别吧!
js
class BasicEffectRulePlugin {
constructor(ruleProperty, effectType) {
this.ruleProperty = ruleProperty;
this.effectType = effectType || ruleProperty;
}
apply(ruleSetCompiler) {
ruleSetCompiler.hooks.rule.tap(
"BasicEffectRulePlugin",
(path, rule, unhandledProperties, result, references) => {
if (unhandledProperties.has(this.ruleProperty)) {
unhandledProperties.delete(this.ruleProperty);
const value = rule[this.ruleProperty];
result.effects.push({
type: this.effectType,
value
});
}
}
);
}
}
2.3.1 构造函数
构造函数的作用十分简洁,保存 ruleProperty, effectType 两个数据。
js
class BasicEffectRulePlugin {
constructor(ruleProperty, effectType) {
this.ruleProperty = ruleProperty;
this.effectType = effectType || ruleProperty;
}
apply(ruleSetCompiler) { /* .... */ }
}
2.3.2 apply 方法
我们看这个插件都做了哪些事情,按照顺序分为 5 个步骤:
js
class BasicEffectRulePlugin {
constructor(ruleProperty, effectType) { /* .... */ }
apply(ruleSetCompiler) {
// 1.
ruleSetCompiler.hooks.rule.tap(
"BasicEffectRulePlugin",
(path, rule, unhandledProperties, result, references) => {
// 2.
if (unhandledProperties.has(this.ruleProperty)) {
// 3.
unhandledProperties.delete(this.ruleProperty);
// 4.
const value = rule[this.ruleProperty];
// 5.
result.effects.push({
type: this.effectType,
value
});
}
}
);
}
}
- 向 ruleSetCompiler.hooks.rule 钩子订阅函数,以下为函数中的细节;
- 判断 unhandledProperties 是否存在 当前 ruleProperty,若存在则处理;
- 从 unhandledProperties 中移除 ruleProperty 属性;
- 从 rule 中取出 ruleProperty 对应的 value;
- 将 ruleProperty 对应的 value 构造成标准 effect 对象加入到 result.effects 数组中;
2.3.3 Effect 与前面 matcher 插件的区别
这个构造应该和 ruleSetCompiler 最终构造的 result 有关,仅从插件 apply 方法看,直观的差异是:
- matcherRulePlugin 的结果加入到 result.conditions 数组,而 effectPlugin 是把结果加入到 result.effects 中;
- 除了目标归属不同,最终的构造结果也不同,matcherRule 插件是把 ruleProperty 经过 ruleSetCompiler 编译成函数的,而 effectPlugin 则不需要编译,直接把结果放到 effects;
大胆推测,condition 的应该是动态调用取结果的,而 effect 的就是静态只读的值;具体是什么我们后面解密 ruleSetCompiler 的相关方法!
三、总结
本文介绍了和 rule 相关的前三个插件------ BasicMatcherRulePlugin、ObjectMatcherRulePlugin、BasicEffectRulePlugin,下面我们看看他们三个的作用和生效的原理:
- BasicMatcherRulePlugin:注册包括 scheme、mimetype 在内的 basic rule,其内部是被 ruleSetCompiler.compileCondition 方法编译成 condition.fn 函数并加入到 result.conditions 中;
- ObjectMatcherRulePlugin:处理对象 rule,其内部是遍历这个对象,把每个属性都编译成一个 conditon.fn 函数加入到 result.conditions;
- BasicEffectRulePlugin:处理 effect rule,这个插件和前两个有所不同,它是直接把 ruleProperty 对应的的 value 加入到 result.effects 中;
webpack 中一共包含了 4 个插件,这里我们讨论了三个,下一篇我们讨论最后一个 UseEffectPlugin!