欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-03-05 19:46:18
上篇文章给大家介绍了    大家可以点击查看。 第一步处理rule为字符串,直接返回一个包装类,很简单看注释就好了。 test   ...

上篇文章给大家介绍了   

大家可以点击查看。

第一步处理rule为字符串,直接返回一个包装类,很简单看注释就好了。

test

  然后处理test、include、exclude,如下:

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));
 }
}

  checkresourcesource直接看源码:

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;
}

  这个用于检测配置来源的唯一性,后面会能看到作用,同样作用的还有checkusesource方法。

  随后将三个参数包装成一个对象传入normalizecondition方法,该方法对常规参数进行函数包装:

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);
 }
}

  这里用js的rules做案例,看这个方法的返回:

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);
 }
}

  这里继续看ormatcher、andmatcher函数的处理:

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;
 };
}

  从字面意思也可以理解函数作用,比如说这里的include被包装成一个ormatcher函数,传入的字符串无论是以数组中任何一个元素开头都会返回true,andmatcher以及未出现的notmatcher同理。

resource

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

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));
 }
}

  可以看出这个参数与前面那个是互斥的,应该是老版api,下面两种方式实现是一样的:

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

  接下来的resourcequery、compiler、issuer先跳过,脚手架没有,不知道怎么写。

loader

  下面是另一块主要配置loader(s),这里loader与loaders作用一样的,当初还头疼啥区别:

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)")));
}

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

  首先normalizeuse方法如下:

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)];
};

  先讲解有options或者query的模式,这里会把参数包装一个对象传入normalizeuse方法:

loader && (options || query)

// 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;
}

  比起对test的处理,这里就简单的多,简述如下:

1、尝试取出options(query)中的ident参数,赋值给newitem的ident,没有就赋值为默认的ref-

2、取出对象中不是options、query的键,赋值给newitem,这里传进来的键只有三个,剩下的就是loader,这个filter是逗我???

3、返回newitem对象

  总之,不知道为什么防止什么意外情况而写出来的垃圾代码,这段代码其实十分简单。

单loader

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

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

返回:

[{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
 };
}

  这种就是串行调用loader的处理方式,代码很简单。

tips

  这里有一点要注意,一旦loader使用了串行调用方式,不要传options或者query参数,不然loader不会被切割解析!!!

  这两种方式只是不同的使用方法,最后的结果都是一样的。

use

  下面是use参数的解析,估计因为这是老版的api,所以格式要求严格,处理比较随便:

if (rule.use) {
 checkusesource("use");
 newrule.use = ruleset.normalizeuse(rule.use, ident);
}

  如果不用loader(s),可以用use替代,但是需要按照格式写,比如说上述串行简写的loader,在use中就需要这样写:

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

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

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

  因为基本上不用了,所以这里简单看一下。

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参数,会填充这个对象,key为ident值,value为对应的options。

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

总结

以上所述是小编给大家介绍的细说webpack源码之compile流程-rules参数处理技巧(2),希望对大家有所帮助