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


Posted in Javascript onDecember 26, 2017

上篇文章给大家介绍了细说webpack源码之compile流程-rules参数处理技巧(1),    细说webpack源码之compile流程-入口函数run

大家可以点击查看。

第一步处理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),希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
javascript 浏览器检测代码精简版
Mar 04 Javascript
javascript new fun的执行过程
Aug 05 Javascript
JQuery 返回布尔值Is()条件判断方法代码
May 14 Javascript
JavaScript实现简单的时钟实例代码
Nov 23 Javascript
JS获取客户端IP地址、MAC和主机名的7个方法汇总
Jul 21 Javascript
jQuery手机拨号界面特效代码分享
Aug 27 Javascript
简单理解js的冒泡排序
Dec 19 Javascript
jQuery Validate 数组 全部验证问题
Jan 12 Javascript
webpack入门必知必会
Jan 16 Javascript
Koa项目搭建过程详细记录
Apr 12 Javascript
JavaScript寄生组合式继承原理与用法分析
Jan 11 Javascript
Vue列表如何实现滚动到指定位置样式改变效果
May 09 Javascript
AngularJS实现的根据数量与单价计算总价功能示例
Dec 26 #Javascript
细说webpack源码之compile流程-rules参数处理技巧(1)
Dec 26 #Javascript
Bootstrap 模态框多次显示后台提交多次BUG的解决方法
Dec 26 #Javascript
细说webpack源码之compile流程-入口函数run
Dec 26 #Javascript
Vue 进入/离开动画效果
Dec 26 #Javascript
node.js中路由,中间件,ge请求和post请求的参数详解
Dec 26 #Javascript
Angular实现可删除并计算总金额的购物车功能示例
Dec 26 #Javascript
You might like
PHP 开源AJAX框架14种
2009/08/24 PHP
YII分模块加载路由的实现方法
2018/10/01 PHP
jquery pagination插件实现无刷新分页代码
2009/10/13 Javascript
利用谷歌地图API获取点与点的距离的js代码
2012/10/11 Javascript
dwz 如何去掉ajaxloading具体代码
2013/05/22 Javascript
js调用iframe实现打印页面内容的方法
2014/03/04 Javascript
举例简介AngularJS的内部语言环境
2015/06/17 Javascript
jquery利用拖拽方式在图片上添加热链接
2015/11/24 Javascript
基于jQuery实现的打字机效果
2017/01/16 Javascript
AngularJS学习第二篇 AngularJS依赖注入
2017/02/13 Javascript
underscore之function_动力节点Java学院整理
2017/07/11 Javascript
简单实现js轮播图效果
2017/07/14 Javascript
Easyui使用Dialog行内按钮布局的实例
2017/07/27 Javascript
JS实现的按钮点击颜色切换功能示例
2017/10/19 Javascript
微信小程序实现点击文字页面跳转功能【附源码下载】
2017/12/12 Javascript
jQuery获取所有父级元素及同级元素及子元素的方法(推荐)
2018/01/21 jQuery
使用Vue构建可重用的分页组件
2018/03/26 Javascript
js+canvas实现图片格式webp/png/jpeg在线转换
2020/08/22 Javascript
跟老齐学Python之画圈还不简单吗?
2014/09/20 Python
python通过colorama模块在控制台输出彩色文字的方法
2015/03/19 Python
Python爬取APP下载链接的实现方法
2016/09/30 Python
Python 模块EasyGui详细介绍
2017/02/19 Python
python使用pil库实现图片合成实例代码
2018/01/20 Python
python jieba分词并统计词频后输出结果到Excel和txt文档方法
2018/02/11 Python
Python 类的特殊成员解析
2018/06/20 Python
python滑块验证码的破解实现
2019/11/10 Python
如何通过python检查文件是否被占用
2020/12/18 Python
HTML5上传文件显示进度的实现代码
2012/08/30 HTML / CSS
德国消费电子产品购物网站:Guter Kauf
2020/09/15 全球购物
梅西百货官网:Macy’s
2020/08/04 全球购物
工业设计专业推荐信
2013/10/29 职场文书
竞选班干部演讲稿400字
2014/08/20 职场文书
暑期培训班招生方案
2014/08/26 职场文书
先进工作者推荐材料
2014/12/23 职场文书
宇宙与人观后感
2015/06/05 职场文书
学前班教学反思
2016/02/24 职场文书