前言

1.主要目的为稍微梳理从配置到装载的流程。另外详解当然要加点源码提升格调(本人菜鸟,有错还请友善指正)

2.被的WebPack打包的文件,都被转化为一个模块,比如import './xxx/x.jpg'或require('./xxx/x.js')。至于具体实际怎么转化,交由装载机处理

3.下文会使用打字稿(劝退警告?)以方便说明有哪些选项和各个选项的值类型

配置语法解析

模块属性

module.exports={...module:{noParse:/jquery/,rules:[{test:/\.js/,exclude:/node_modules/,use:[{loader:'./loader1.js?num=1',options:{myoptions:false},},"./loader2.js?num=2",]},{test:/\.js/,include:/src/,loader:'./loader1.js!./loader2.js',},]}}


上述是展示常见的配置写法.webpack为其选项都编写了打字稿声明,这个模块属性的声明在的WebPack /声明中可见:

exportinterfaceModuleOptions{//一般下面这两个noParse?:RegExp[]|RegExp|Function|string[]|string;rules?:RuleSetRules;//这些...已被废弃,即将被删除,不用看defaultRules?:RuleSetRules;exprContextCritical?:boolean;exprContextRecursive?:boolean;exprContextRegExp?:boolean|RegExp;exprContextRequest?:string;strictExportPresence?:boolean;strictThisContextOnImports?:boolean;unknownContextCritical?:boolean;unknownContextRecursive?:boolean;unknownContextRegExp?:boolean|RegExp;unknownContextRequest?:string;unsafeCache?:boolean|Function;wrappedContextCritical?:boolean;wrappedContextRecursive?:boolean;wrappedContextRegExp?:RegExp;}

noParse 用于让的WebPack跳过对这些文件的转化,也就是他们不会被加载程序所处理(但还是会被打包并输出到DIST目录)

rules 核心配置,见下文

module.rules属性

module.rules类型是RuleSetRule[],请继续的WebPack /声明查看其打字稿,有哪些属性,属性类型一目了然。

注意RuleSetConditionsRecursive这个东西在另外一个文件声明,是interface RuleSetConditionsRecursive extends Array<import("./declarations/WebpackOptions").RuleSetCondition> {},其实就是export type RuleSetConditionsRecursive = RuleSetCondition[];,代表一个RuleSetCondition数组

意义直接贴中文文档:模块。

好了,上面基本是搬运打字稿声明,结合文档基本能知道有哪些属性,属性的类型和含义。下面结合源码对文档一些难以理解的地方补充说明。

正文

规则集

规则的规范化(类型收敛)

由上可知一个规则对象,其属性类型有多种可能,所以应该对其规范化,底层减少代码的大量typeof等判断。这是由RuleSet.js进行规范化的。下面是经过规则集处理后的一个规则对象大致形式:

//rule对象规范化后的形状应该是:{resource:function(),resourceQuery:function(),compiler:function(),issuer:function(),use:[{loader:string,options:string|object,//源码的注释可能是历史遗留原因,options也可为object类型<any>:<any>}//下文称呼这个为use数组的单个元素为loader对象,规范化后它一般只有loader和options属性],rules:[<rule>],oneOf:[<rule>],<any>:<any>,}

rules状语从句:oneOf的英文用来嵌套的,里面的也是规范过的规则对象。

它这里的四个函数是的WebPack用来判断是否需要把文件内容交给装载器处理的。如的WebPack遇到了import './a.js',那么rule.resource('f:/a.js')===true时会才把文件交由规则中指定的装载机去处理,resourceQuery等同理。

的这里的传入参数'f:/a.js'就是官网所说的

条件已经两个输入值:

资源:请求文件的绝对路径。它已经根据resolve规则解析。issuer :被请求资源(请求的资源)的模块文件的绝对路径。是导入时的位置。

首先要做的是把Rule.loader, ,Rule.options(Rule.query已废弃,但尚未删除),移动全部到Rule.use数组元素的对象里。主要这由static normalizeRule(rule, refs, ident)函数处理,代码主要是处理各种“简写”,把值搬运到装载器对象,做一些报错处理,难度不大看一下即可,下面挑它里面的“条件函数”规范化来说一说。

Rule.resource规范化

由上可知这是一个“条件函数”,它是根据我们的配置中的test,include,exclude,resource规范化而生成的源码180多行中:

if(rule.test||rule.include||rule.exclude){checkResourceSource("test+include+exclude");condition={test:rule.test,include:rule.include,exclude:rule.exclude};try{newRule.resource=RuleSet.normalizeCondition(condition);}catch(error){thrownewError(RuleSet.buildErrorMessage(condition,error));}}if(rule.resource){checkResourceSource("resource");try{newRule.resource=RuleSet.normalizeCondition(rule.resource);}catch(error){thrownewError(RuleSet.buildErrorMessage(rule.resource,error));}}

中文档说Rule.test的英文Rule.resource.test的简写,实际就是这串代码。

checkResourceSource用来检查是否重复配置,即文档中提到的:你如果提供了一个Rule.test选项对话,就不能再提供Rule.resource

求最后RuleSet.normalizeCondition生成一个“条件函数”,如下:

staticnormalizeCondition(condition){if(!condition)thrownewError("Expectedconditionbutgotfalsyvalue");if(typeofcondition==="string"){returnstr=>str.indexOf(condition)===0;}if(typeofcondition==="function"){returncondition;}if(conditioninstanceofRegExp){returncondition.test.bind(condition);}if(Array.isArray(condition)){constitems=condition.map(c=>RuleSet.normalizeCondition(c));returnorMatcher(items);}if(typeofcondition!=="object"){throwError("Unexcepted"+typeofcondition+"whenconditionwasexpected("+condition+")");}constmatchers=[];Object.keys(condition).forEach(key=>{constvalue=condition[key];switch(key){case"or":case"include":case"test":if(value)matchers.push(RuleSet.normalizeCondition(value));break;case"and":if(value){constitems=value.map(c=>RuleSet.normalizeCondition(c));matchers.push(andMatcher(items));}break;case"not":case"exclude":if(value){constmatcher=RuleSet.normalizeCondition(value);matchers.push(notMatcher(matcher));}break;default:thrownewError("Unexceptedproperty"+key+"incondition");}});if(matchers.length===0){thrownewError("Exceptedconditionbutgot"+condition);}if(matchers.length===1){returnmatchers[0];}returnandMatcher(matchers);}

这串代码主要就是根据字符串,正则表达式,对象,功能类型来生成不同的“条件函数”,难度不大。

notMatcher,orMatcher,andMatcher这三个是辅助函数,看名字就知道了,实现上非常简单,不贴源码了。有什么不明白的逻辑,代入进去跑一跑就知道了

规则使用规范化

我们接下来要把Rule.use给规范分类中翻译上面提到的那种形式,即让装载机只对象保留loader状语从句:options这两个属性(当然,并不是它一定只有这两个属性)源码如下:

staticnormalizeUse(use,ident){if(typeofuse==="function"){returndata=>RuleSet.normalizeUse(use(data),ident);}if(Array.isArray(use)){returnuse.map((item,idx)=>RuleSet.normalizeUse(item,`${ident}-${idx}`)).reduce((arr,items)=>arr.concat(items),[]);}return[RuleSet.normalizeUseItem(use,ident)];}staticnormalizeUseItemString(useItemString){constidx=useItemString.indexOf("?");if(idx>=0){return{loader:useItemString.substr(0,idx),options:useItemString.substr(idx+1)};}return{loader:useItemString,options:undefined};}staticnormalizeUseItem(item,ident){if(typeofitem==="string"){returnRuleSet.normalizeUseItemString(item);}constnewItem={};if(item.options&&item.query){thrownewError("Providedoptionsandqueryinuse");}if(!item.loader){thrownewError("Noloaderspecified");}newItem.options=item.options||item.query;if(typeofnewItem.options==="object"&&newItem.options){if(newItem.options.ident){newItem.ident=newItem.options.ident;}else{newItem.ident=ident;}}constkeys=Object.keys(item).filter(function(key){return!["options","query"].includes(key);});for(constkeyofkeys){newItem[key]=item[key];}returnnewItem;}

这几个函数比较绕,但总体来说难度不大。

这里再稍微总结几点现象:

1.loader: './loader1!./loader2',如果在Rule.loader指明了两个以以上装载机,那么不可设置Rule.options,因为不知道该把这个选项传给哪个装载机,直接报错

2.-loader不可省略,如babel!./loader的英文非法的,因为在webpack/lib/NormalModuleFactory.js440行左右,已经不再支持这种写法,直接报错叫你写成babel-loader

3.loader: './loader1?num1=1&num2=2'将被处理成{loader: './loader', options: 'num=1&num=2'},以?进行了字符串分割,最终处理成规范化装载机对象

规则集规范化到此结束,有兴趣的可以继续围观源码的高管方法和构造函数

装载机

接下来算是番外,讨论各种装载机如何读取我们配置的对象。

**属性在的WebPack的传递与处理选项**

首先一个装载机就是简单的导出一个函数即可,比如上面举例用到的

loader1.js:module.exports=function(content){console.log(this)console.log(content)returncontent}

这个函数里面的这个被绑定到一个loaderContext(loader上下文)中,官方api:loader API。

直接把这个loader1.js加入到配置文件webpack.config.js里面即可,在编译时他就会打印出一些东西。

简单而言,就是在装载机中,可以我们通过this.query来访问到规范化装载机对象options属性。比如{loader: './loader1.js', options: 'num1=1&num=2'},那么this.query === '?num1=1&num=2'。

问题来了,这个问号哪里来的如果它是一个对象?

的WebPack通过装载机的领先者来执行装载机,这个问题可以去loader-runner/lib/LoaderRunner.js,在createLoaderObject函数中有这么一段:

if(obj.options===null)obj.query="";elseif(obj.options===undefined)obj.query="";elseif(typeofobj.options==="string")obj.query="?"+obj.options;elseif(obj.ident){obj.query="??"+obj.ident;}elseif(typeofobj.options==="object"&&obj.options.ident)obj.query="??"+obj.options.ident;elseobj.query="?"+JSON.stringify(obj.options);

在以及runLoaders函数里面的这段:

Object.defineProperty(loaderContext,"query",{enumerable:true,get:function(){varentry=loaderContext.loaders[loaderContext.loaderIndex];returnentry.options&&typeofentry.options==="object"?entry.options:entry.query;}});

总结来说,当选项存在且是一个对象时,那么this.query就是这个对象;如果选项是一个字符串,那么this.query等于一个问号+这个字符串

多数装载机读取选项的方法

constloaderUtils=require('loader-utils')module.exports=function(content){console.log(loaderUtils.getOptions(this))returncontent}

借助架utils的读取那么接下来走进loaderUtils.getOptions看看:

constquery=loaderContext.query;if(typeofquery==='string'&&query!==''){returnparseQuery(loaderContext.query);}if(!query||typeofquery!=='object'){returnnull;}returnquery;

这里只复制了关键代码,它主要是做一些简单判断,对字符串的核心转换在parseQuery上,接着看:

constJSON5=require('json5');functionparseQuery(query){if(query.substr(0,1)!=='?'){thrownewError("AvalidquerystringpassedtoparseQueryshouldbeginwith'?'");}query=query.substr(1);if(!query){return{};}if(query.substr(0,1)==='{'&&query.substr(-1)==='}'){returnJSON5.parse(query);}constqueryArgs=query.split(/[,&]/g);constresult={};queryArgs.forEach((arg)=>{constidx=arg.indexOf('=');if(idx>=0){letname=arg.substr(0,idx);letvalue=decodeURIComponent(arg.substr(idx+1));if(specialValues.hasOwnProperty(value)){value=specialValues[value];}if(name.substr(-2)==='[]'){name=decodeURIComponent(name.substr(0,name.length-2));if(!Array.isArray(result[name])){result[name]=[];}result[name].push(value);}else{name=decodeURIComponent(name);result[name]=value;}}else{if(arg.substr(0,1)==='-'){result[decodeURIComponent(arg.substr(1))]=false;}elseif(arg.substr(0,1)==='+'){result[decodeURIComponent(arg.substr(1))]=true;}else{result[decodeURIComponent(arg)]=true;}}});returnresult;}

使用了json5库,以及自己的一套参数的转换。

总结来说,只要你能确保自己使用的装载器是通过loader-utils来获取选项对象的,那么你可以直接给选项写成如下字符串(inline loader中常用,如import 'loader1?a=1&b=2!./a.js'):

options:"{a:'1',b:'2'}"//非json,是json5格式字符串,略有出入,请右转百度options:"list[]=1&list=2[]&a=1&b=2"//http请求中常见的url参数部分

更多示例可在的WebPack /架utils的中查看