前言
主要目的为稍微梳理从配置到装载的流程。另外详解当然要加点源码提升格调(本人菜鸟,有错还请友善指正)
被的WebPack打包的文件,都被转化为一个模块,比如import './xxx/x.jpg'或require('./xxx/x.js')。至于具体实际怎么转化,交由装载机处理
下文会使用打字稿(劝退警告?)以方便说明有哪些选项和各个选项的值类型
配置语法解析
模块属性
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 /声明中可见:
export interface ModuleOptions { // 一般下面这两个 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) { throw new Error(RuleSet.buildErrorMessage(condition, error)); }}if (rule.resource) { checkResourceSource("resource"); try { newRule.resource = RuleSet.normalizeCondition(rule.resource); } catch (error) { throw new Error(RuleSet.buildErrorMessage(rule.resource, error)); }}
中文档说Rule.test的英文Rule.resource.test的简写,实际就是这串代码。
checkResourceSource用来检查是否重复配置,即文档中提到的:你如果提供了一个Rule.test选项对话,就不能再提供Rule.resource
求最后RuleSet.normalizeCondition生成一个“条件函数”,如下:
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; } if (condition instanceof RegExp) { return condition.test.bind(condition); } 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 = []; 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);}
这串代码主要就是根据字符串,正则表达式,对象,功能类型来生成不同的“条件函数”,难度不大。
notMatcher,orMatcher,andMatcher这三个是辅助函数,看名字就知道了,实现上非常简单,不贴源码了。有什么不明白的逻辑,代入进去跑一跑就知道了
规则使用规范化
我们接下来要把Rule.use给规范分类中翻译上面提到的那种形式,即让装载机只对象保留loader状语从句:options这两个属性(当然,并不是它一定只有这两个属性)源码如下:
static normalizeUse(use, ident) { if (typeof use === "function") { return data => RuleSet.normalizeUse(use(data), ident); } if (Array.isArray(use)) { return use .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`)) .reduce((arr, items) => arr.concat(items), []); } return [RuleSet.normalizeUseItem(use, ident)];}static normalizeUseItemString(useItemString) { const idx = useItemString.indexOf("?"); if (idx >= 0) { return { loader: useItemString.substr(0, idx), options: useItemString.substr(idx + 1) }; } return { loader: useItemString, options: undefined };}static normalizeUseItem(item, ident) { 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; if (typeof newItem.options === "object" && newItem.options) { if (newItem.options.ident) { newItem.ident = newItem.options.ident; } else { newItem.ident = ident; } } const keys = Object.keys(item).filter(function(key) { return !["options", "query"].includes(key); }); for (const key of keys) { newItem[key] = item[key]; } return newItem;}
这几个函数比较绕,但总体来说难度不大。
这里再稍微总结几点现象:
loader: './loader1!./loader2',如果在Rule.loader指明了两个以以上装载机,那么不可设置Rule.options,因为不知道该把这个选项传给哪个装载机,直接报错
-loader不可省略,如babel!./loader的英文非法的,因为在webpack/lib/NormalModuleFactory.js440行左右,已经不再支持这种写法,直接报错叫你写成babel-loader
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) return content}
这个函数里面的这个被绑定到一个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 = "";else if (obj.options === undefined) obj.query = "";else if (typeof obj.options === "string") obj.query = "?" + obj.options;else if (obj.ident) { obj.query = "??" + obj.ident;}else if (typeof obj.options === "object" && obj.options.ident) obj.query = "??" + obj.options.ident;else obj.query = "?" + JSON.stringify(obj.options);
在以及runLoaders函数里面的这段:
Object.defineProperty(loaderContext, "query", { enumerable: true, get: function() { var entry = loaderContext.loaders[loaderContext.loaderIndex]; return entry.options && typeof entry.options === "object" ? entry.options : entry.query; }});
总结来说,当选项存在且是一个对象时,那么this.query就是这个对象;如果选项是一个字符串,那么this.query等于一个问号+这个字符串
多数装载机读取选项的方法
const loaderUtils=require('loader-utils')module.exports = function (content){ console.log(loaderUtils.getOptions(this)) return content}
借助架utils的读取那么接下来走进loaderUtils.getOptions看看:
const query = loaderContext.query;if (typeof query === 'string' && query !== '') { return parseQuery(loaderContext.query);}if (!query || typeof query !== 'object') { return null;}return query;
这里只复制了关键代码,它主要是做一些简单判断,对字符串的核心转换在parseQuery上,接着看:
const JSON5 = require('json5');function parseQuery(query) { if (query.substr(0, 1) !== '?') { throw new Error( "A valid query string passed to parseQuery should begin with '?'" ); } query = query.substr(1); if (!query) { return {}; } if (query.substr(0, 1) === '{' && query.substr(-1) === '}') { return JSON5.parse(query); } const queryArgs = query.split(/[,&]/g); const result = {}; queryArgs.forEach((arg) => { const idx = arg.indexOf('='); if (idx >= 0) { let name = arg.substr(0, idx); let value = 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; } else if (arg.substr(0, 1) === '+') { result[decodeURIComponent(arg.substr(1))] = true; } else { result[decodeURIComponent(arg)] = true; } } }); return result;}
使用了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的中查看