细说webpack源码之compile流程-rules参数处理技巧(2)

作者:构建工坊时间:2026-10-23 02:30:00浏览:106

本文深入解析Webpack源码中compile阶段的rules参数处理逻辑。详细拆解test、include、exclude如何被normalizeCondition包装为匹配函数,以及resource、resourceQuery等参数的校验机制。重点分析loader与use配置在normalizeUse中的处理差异,包括options、query及ident参数的解析,以及串行loader字符串的切割与扁平化过程,揭示配置简单但处理复杂的源码实现细节。

在上一篇《细说webpack源码之compile流程-rules参数处理技巧(1)》以及《细说webpack源码之compile流程-入口函数run》中,我们介绍了相关背景,大家可以点击查看。

本文将继续深入探讨rules参数的具体处理流程。

test、include与exclude参数处理

第一步是处理rule为字符串的情况,此时直接返回一个包装类,逻辑相对简单,具体实现可参考代码注释。

接下来处理test、include和exclude参数,核心逻辑如下所示。

其中checkResourceSource函数用于检测配置来源的唯一性,后续代码中会看到其作用,具有相同作用的还有checkUseSource方法。


if (rule.test || rule.include || rule.exclude) {
 // 标记使用参数
 checkResourceSource("test + include + exclude");
 // 没有就是undefined
 condition = {
 test: rule.test,
 include: rule.include,
 exclude: rule.exclude
 };
 // 处理常规参数
 try {
 newRule.resource = RuleSet.normalizeCondition(condition);
 } catch (error) {
 throw new Error(RuleSet.buildErrorMessage(condition, error));
 }
}

随后,这三个参数会被包装成一个对象传入normalizeCondition方法,该方法负责对常规参数进行函数包装:


let resourceSource;
// ...
function checkResourceSource(newSource) {
 // 第一次直接跳到后面赋值
 if (resourceSource && resourceSource !== newSource)
 throw new Error(RuleSet.buildErrorMessage(rule, new Error("Rule can only have one resource source (provided " + newSource + " and " + resourceSource + ")")));
 resourceSource = newSource;
}

这里以js的rules配置为例,观察该方法的返回结果:

继续分析orMatcher和andMatcher函数的处理逻辑:


class RuleSet {
 constructor(rules) { /**/ };
 static normalizeCondition(condition) {
 // 假值报错
 if (!condition) throw new Error("Expected condition but got falsy value");
 // 检测给定字符串是否以这个开头
 if (typeof condition === "string") { return str => str.indexOf(condition) === 0; }
 // 函数直接返回
 if (typeof condition === "function") { return condition; }
 // 正则表达式返回一个正则的test函数
 if (condition instanceof RegExp) { return condition.test.bind(condition); }
 // 数组map递归处理 有一个满足返回true
 if (Array.isArray(condition)) {
  const items = condition.map(c => RuleSet.normalizeCondition(c));
  return orMatcher(items);
 }
 if (typeof condition !== "object") throw Error("Unexcepted " + typeof condition + " when condition was expected (" + condition + ")");
 const matchers = [];
 // 对象会对每个值进行函数包装弹入matchers中
 Object.keys(condition).forEach(key => {
  const value = condition[key];
  switch (key) {
  case "or":
  case "include":
  case "test":
   if (value)
   matchers.push(RuleSet.normalizeCondition(value));
   break;
  case "and":
   if (value) {
   const items = value.map(c => RuleSet.normalizeCondition(c));
   matchers.push(andMatcher(items));
   }
   break;
  case "not":
  case "exclude":
   if (value) {
   const matcher = RuleSet.normalizeCondition(value);
   matchers.push(notMatcher(matcher));
   }
   break;
  default:
   throw new Error("Unexcepted property " + key + " in condition");
  }
 });
 if (matchers.length === 0)
  throw new Error("Excepted condition but got " + condition);
 if (matchers.length === 1)
  return matchers[0];
 return andMatcher(matchers);
 }
}

从函数名称即可理解其作用。例如,这里的include被包装成一个orMatcher函数,传入的字符串只要以数组中任何一个元素开头,就会返回true。andMatcher以及未出现的notMatcher也遵循类似逻辑。


class RuleSet {
 constructor(rules) { /**/ };
 /*
 Example:
 {
  test: /\.js$/,
  loader: 'babel-loader',
  include: [resolve('src'), resolve('test')]
 }
 */
 /*
 condition:
 {
  test: /\.js$/,
  include: ['d:\\workspace\\src', 'd:\\workspace\\test'],
  exclude: undefined
 }
 */
 static normalizeCondition(condition) {
 // include返回类似于 [(str) => str.indexOf('d:\\workspace\\src') === 0,...] 的函数
 if (typeof condition === "string") { return str => str.indexOf(condition) === 0; }
 // test参数返回了 /\.js$/.test 函数
 if (condition instanceof RegExp) { return condition.test.bind(condition); }
 // include为数组
 if (Array.isArray(condition)) {
  const items = condition.map(c => RuleSet.normalizeCondition(c));
  return orMatcher(items);
 }
 const matchers = [];
 // 解析出['test','include','exclude']
 Object.keys(condition).forEach(key => {
  const value = condition[key];
  switch (key) {
  // 此value为一个数组
  case "include":
  case "test":
   if (value)
   matchers.push(RuleSet.normalizeCondition(value));
   break;
  // undefined跳过
  case "exclude":
   if (value) { /**/ }
   break;
  default:
   throw new Error("Unexcepted property " + key + " in condition");
  }
 });
 return andMatcher(matchers);
 }
}

下面是对resource参数的处理,源码如下:


function orMatcher(items) {
 // 当一个函数被满足条件时返回true
 return function(str) {
 for (let i = 0; i < items.length; i++) {
  if (items[i](str))
  return true;
 }
 return false;
 };
}

function andMatcher(items) {
 // 当一个条件不满足时返回false
 return function(str) {
 for (let i = 0; i < items.length; i++) {
  if (!items[i](str))
  return false;
 }
 return true;
 };
}

可以看出,这个参数与前一个(test/include/exclude)是互斥的,这属于较老版本的API。下面两种方式实现的效果是一样的:

接下来的resourceQuery、compiler和issuer参数暂时跳过,因为脚手架中未使用,具体写法尚不明确。

resource参数处理

接下来是另一块主要配置:loader(s)。这里loader与loaders的作用相同,之前可能曾疑惑两者的区别。


if (rule.resource) {
 // 如果前面检测到了test || include || exclude参数 这里会报错
 checkResourceSource("resource");
 try {
 newRule.resource = RuleSet.normalizeCondition(rule.resource);
 } catch (error) {
 throw new Error(RuleSet.buildErrorMessage(rule.resource, error));
 }
}

之前举例的babel-loader属于第一种单loader配置,这里使用vue-loader嵌套css配置作为示例。


/*
方式1:
 rules:[
 {
  test: /\.js$/,
  loader: 'babel-loader',
  include: [resolve('src'), resolve('test')]
 }
 ]
*/
/*
方式2:
 rules:[
 {
  resource:{
  test: /\.js$/,
  include: [resolve('src'), resolve('test')]
  exclude: undefined
  }
 }
 ]
*/

首先,normalizeUse方法的实现如下:

loader与use配置解析

先讲解包含options或query的模式,这里会将参数包装成一个对象传入normalizeUse方法:

loader && (options || query)


const loader = rule.loaders || rule.loader;
// 单loader情况
if (typeof loader === "string" && !rule.options && !rule.query) {
 checkUseSource("loader");
 newRule.use = RuleSet.normalizeUse(loader.split("!"), ident);
}
// loader配合options或query出现
else if (typeof loader === "string" && (rule.options || rule.query)) {
 checkUseSource("loader + options/query");
 newRule.use = RuleSet.normalizeUse({
 loader: loader,
 options: rule.options,
 query: rule.query
 }, ident);
}
// options与query同时出现报错
else if (loader && (rule.options || rule.query)) {
 throw new Error(RuleSet.buildErrorMessage(rule, new Error("options/query cannot be used with loaders (use options for each array item)")));
}
/*
 处理这种愚蠢用法时:
 {
 test: /\.css$/,
 loader: [{ loader: 'less-loader' }, { loader: 'css-loader' }]
 }
*/
else if (loader) {
 checkUseSource("loaders");
 newRule.use = RuleSet.normalizeUse(loader, ident);
}
// 单独出现options或者query报错
else if (rule.options || rule.query) {
 throw new Error(RuleSet.buildErrorMessage(rule, new Error("options/query provided without loader (use loader + options)")));
}

与处理test相比,这里逻辑简单得多,简述如下:

1、尝试取出options(或query)中的ident参数,赋值给newItem的ident,若没有则赋值为默认的ref-。


static normalizeUse(use, ident) {
 // 单loader字符串
 if (Array.isArray(use)) {
 return use
  // 如果是单loader情况这里会返回[[loader1...],[loader2...]]
  .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`))
  // 扁平化后变成[loader1,loader2]
  .reduce((arr, items) => arr.concat(items), []);
 }
 // 对象或字符串
 return [RuleSet.normalizeUseItem(use, ident)];
};

2、取出对象中非options、query的键,赋值给newItem。这里传进来的键只有三个,剩下的即为loader,此处的filter逻辑看似多余。

3、返回newItem对象。


// indet => 'ref-'
static normalizeUseItem(item, ident) {
 if (typeof item === "function")
 return item;
 if (typeof item === "string") {
 return RuleSet.normalizeUseItemString(item);
 }
 const newItem = {};
 if (item.options && item.query) throw new Error("Provided options and query in use");
 if (!item.loader) throw new Error("No loader specified");
 newItem.options = item.options || item.query;
 // 防止options:null的情况
 if (typeof newItem.options === "object" && newItem.options) {
 // 这里只是为了处理ident参数
 if (newItem.options.ident)
  newItem.ident = newItem.options.ident;
 else
  newItem.ident = ident;
 }
 // 取出loader参数
 const keys = Object.keys(item).filter(function(key) {
 return ["options", "query"].indexOf(key) < 0;
 });
 keys.forEach(function(key) {
 newItem[key] = item[key];
 });
 /*
 newItem = 
 {
 loader:'原字符串',
 ident:'ref-', (或自定义)
 options:{...}
 }
 */
 return newItem;
}

总之,这段代码不知为何为防止意外情况而编写,显得有些冗余,但整体逻辑其实十分简单。

第二种情况是单字符串配置,会对'!'进行切割,调用map方法处理,再调用reduce方法扁平化为一个数组。直接查看normalizeUseItemString方法:

返回结果如下:

这代表了串行调用loader的处理方式,代码逻辑较为简单。

这里有一点需要注意:一旦loader使用了串行调用方式,不要传递options或者query参数,否则loader不会被正确切割解析。

这两种方式只是使用方法不同,最终结果是一致的。

下面是use参数的解析,由于这属于老版API,格式要求严格,因此处理相对随意:


/*
Example:
{
 test: /\.css$/,
 loader: 'css-loader?{opt:1}!style-loader'
}

如果不用loader(s),可以用use替代,但需要按照特定格式书写。例如上述串行简写的loader,在use中需要这样写:


[{loader:'css-loader',options:{opt:1}},
{loader:'style-loader'}]
*/
static normalizeUseItemString(useItemString) {
 // 根据'?'切割获取loader的参数
 const idx = useItemString.indexOf("?");
 if (idx >= 0) {
 return {
  loader: useItemString.substr(0, idx),
  // 后面的作为options返回
  options: useItemString.substr(idx + 1)
 };
 }
 return {
 loader: useItemString
 };
}

而对应options或query,需要这样写:

由于基本不再使用,这里仅作简要查看。

这两个配置项使用较少,也没有特别难理解的地方。

下一步是过滤出未处理的参数,并添加到newRule上:

基本上用到的是test、loader和options,暂时不清楚是否有额外参数。

最后这个地方终于用到了传进来的纯净对象refs。


if (rule.use) {
 checkUseSource("use");
 newRule.use = RuleSet.normalizeUse(rule.use, ident);
}

如果在options中传了ident参数,会填充这个对象,key为ident值,value为对应的options。


/*
 {
 test:/\.css$/,
 user:['css-loader','style-loader]
 }
*/

至此,所有rules的规则已经解析完毕,可谓配置简单但处理复杂。


/*
 {
 test:/\.css$/,
 user:{
  loader:'css-loader',
  options:'1'
 }
 }
*/

以上所述是小编给大家介绍的细说webpack源码之compile流程-rules参数处理技巧(2),希望对大家有所帮助。如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!


rules、oneOf
if (rule.rules)
 newRule.rules = RuleSet.normalizeRules(rule.rules, refs, `${ident}-rules`);
if (rule.oneOf)
 newRule.oneOf = RuleSet.normalizeRules(rule.oneOf, refs, `${ident}-oneOf`);

这两项使用频率较低,理解起来并无难度。

接下来需筛选出尚未处理的参数,并将其纳入 newRuls 集合:


const keys = Object.keys(rule).filter((key) => {
 return ["resource", "resourceQuery", "compiler", "test", "include", "exclude", "issuer", "loader", "options", "query", "loaders", "use", "rules", "oneOf"].indexOf(key) < 0;
});
keys.forEach((key) => {
 newRule[key] = rule[key];
});

实际应用中主要涉及 test、loader 及 options 参数,目前未见其他额外参数。

ident


// 防止rules:[]的情况
if (Array.isArray(newRule.use)) {
 newRule.use.forEach((item) => {
 // ident来源于options/query的ident参数
 if (item.ident) {
  refs[item.ident] = item.options;
 }
 });
}

此处最终使用了传入的纯净对象 refs。

若在 options 中指定了 ident 参数,则该对象会被填充,其键名为 ident 的值,值对应相应的 options。

至此,所有 rules 规则解析工作完成,可谓配置简单而处理逻辑复杂。

总结

以上内容为小编分享的细说 webpack 源码之 compile 流程-rules 参数处理技巧(2),希望能对大家有所帮助。如有任何疑问,欢迎留言,小编将及时回复。同时,衷心感谢大家对本站的支持!

作者最新文章