一、前文回顾
上文介绍了和 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!
二、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
}
});
}
}
);
}
}
- 下面我们整体感受一下各个步骤都做了什么工作:
- 订阅 ruleSetCompiler.hooks.rule 钩子,以下为订阅钩子的逻辑;
- 声明 conflictWith 方法,该方法输出属性冲突的错误;
- 判断 unhandledProperties 中是否存在 use 属性,如果有则进行以后的操作;
- 删除 use 和 enforce 属性;
- 处理 loader 和 use 两个属性的冲突,接着处理另一对冲突的属性 options 和 use 属性,之所处理冲突是因为这两对属性不能同时配置,彼此是互斥的;
- 获取 rule.use 和 rule.enforce 属性,分别保存到 use 和 enforce 常量;
- 根据是否存咋 rule.enforce 生成 type 类型,如果有就是 "use-pre" 或者 "use-post" 这种强制某种规则的前提下前置或者后置使用该 loader,如果不配置 enforce 则 type 就是 "use";
- 声明 useToEffect 方法,该方法作用及细节下面详述;
- 声明 useToEffectRaw 方法,该方法原理下面详述;
- 声明 useToEffectsWithoutIdent 方法,原理下面详述;
- 声明 useToEffects 方法,该方法原理下面详述;
- 判断 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 中; - 获取 rule.loader/options/enforce 三个属性,缓存到 loader、options、enforce 三个常量;
- 判断 loader 中是否包含 "!",如果有则进行报错提示?为啥有这个限制,这是因为 webpack 不允许在配置的 loader 中再内联其他loader,比如你这么配置:{ loader: 'style-loader!css-loader!some-other-loader' },这种语法将会被 webpack 认定非法!
- 判断 loader 中是否包含 "?" 这个字符串,如果有则进行报错提示?之所以有这个限制,这是因为 webpack 不允许在配置的 loader 中加入查询字符串,例如 { loader: 'style-loader?root=xyz&test=abc' } 这种语法将会被 webpack 认定为非法,正确做法就是通过 options 进行数据传递;
- 判定 options 是否为 字符串类型,如果使用字符串,同样会被 webpack 认定为非法类型;
- 计算 ident 标识;
- 缓存 ident 和 options;
- 最后将经过表转化处理的对象:{ 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`
);
}
};
方法内容很简单,这里面就是一个报错提示;但是需要注意的是这些冲突的属性:
- "loader" 和 "use",不可以在同一个 rule 中同时配置;
- "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);
}
};
- 判断 item 是否为函数类型;
- 如果是则返回一个函数,这个函数接收data,返回 useToEffectsWithoutIdent 方法的返回值;
- 如不是函数数据类型,则返回 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
}
};
}
};
- 判断 item 是否为字符串,如果是则返回 effect 对象:{ type, value: { loader: item } };
- 如果不是字符串,说明 item 是个对象,这个时候从 item 上获取 loader、options、ident 三个属性值;
- 判断 options 是否为对象,如果是则换成 ident 和 options;
- 判断 options 是否是字符串,如果是则抛出一个废弃警告,因为 options 以及不可以穿字符串了;
- 最后返回标准 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)];
};
- 判断传入的 items 是否是数组,如果是数组,则把数组中的每一项调用 useToEffect 变成标准的 effect 对象;
- 这里说明 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 生效的原来!