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 /声明中可见:
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;
}
这几个函数比较绕,但总体来说难度不大。
这里再稍微总结几点现象:
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)
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的中查看
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
原文链接:http://blog.itpub.net/69946034/viewspace-2658246/