一、前文回顾
上文主要讲述了 resolve 流程中解析 request 的工作,这部分工作主要包含两方面:
- 处理 match-resource 语法(!=!),这种语法是 webpack v4 之后支持的一种新语法,用于在编译中暴力修改原有 request 应该使用的 loader。值得注意的是,这种语法在业务中不要用,这个东西有点危险!这一步主要是获取 matchResourceData,并且将 matchResource 从原有的 request 上移除以便进行后续的解析工作。
- 解析 requestWithoutMatchResource,分析 request 开头的字符是不是
!/-!/!!来决定是不是禁用调用配置中的loader,最后将 loader 和资源路径分离;
经过这两个步骤后我们得到 request 上带有的 内联 loader(elements) 和 模块资源路径(unresolvedResouce);
今天这篇小作文我们将接着讲处理 request 之后,解析内联 loader 的事儿!
二、解析行内 loader 路径
前面我们已经从 request 上得到了指定的行内 loader,下面就要调用 resolver 的能力解析这些 loader 的具体路径,下面是这部分简化后的代码框架:
class NormalModuleFactory extends ModuleFactory {
constructor () {
this.hooks.resolve.tapAsync(
{
name: "NormalModuleFactory",
stage: 100
},
(data, callback) => {
// ...
// 1.
let loaders;
// 2.
const continueCallback = needCalls(2, err => {
// ...
});
// 3.
this.resolveRequestArray(
contextInfo,
contextScheme ? this.context : context,
elements,
loaderResolver,
resolveContext,
(err, result) => {
// 4.
loaders = result;
// 5.
continueCallback();
}
);
}
);
}
}
整体上我把这部分分为了 5 个步骤:
- 声明 loaders 变量,不设初始值;
- 声明 continueCallback 函数,该函数是 needCall 的返回值,这个回调函数会最后被调用;
- 调用 this.resolveRequestArray() 方法,传入包含内联 loader 的数组 elements 以及用于解析 loader 用的 resolver —— loaderResolver,另外还传入了受理解析结果的回调函数;
- 在 this.resolveRequestArray() 方法的回调中,将解析所得到 result 赋值给前面声明的 loaders 变量;
- 调用 2. 声明的 continueCallback 函数继续后续流程;
2.1 needCall
这个方法是个机制的方法,webpack 内部有很多类似抖机灵的解决方案。我们来看看这个函数:
const needCalls = (times, callback) => {
return err => {
if (--times === 0) {
return callback(err);
}
if (err && times > 0) {
times = NaN;
return callback(err);
}
};
};
函数内部十分简洁,接收一个 times 的计数和 times 调用结束后需要调用的 callback;最终返回一个回调函数。
返回值函数内部记录调用次数,每次调用执行 --times 的操作,为0是调用 callback,这么简单的逻辑相信代码初学者都可以组织好,但是为啥要讲它呢?
先思考一个问题:为啥 webpack 要这么做呢,为什么不用 Promise ...,为啥不用 asyncLib 啥的调度....啥的。。。?
优点主要有两点:
- 这是因为 webpack 要性能优化,一个简单的计数器能搞定的事儿就不用更消耗内存的其他实现方式;
- 另外这里是一个典型的闭包场景,随着 callback 被调用,由 needCall 所创建的所有有的执行栈被清空,内存也可以得到及时的释放。
2.2 nmf.resolveRequestArray
该方法由 NormalModuleFactory 类型在原型上实现,用于在 resolve 阶段 并发 解析 loader 和资源模块的路径,简化后的代码如下:
class NormalModuleFactory extends ModuleFactory {
resolveRequestArray(
contextInfo,
context,
array,
resolver,
resolveContext,
callback
) {
// 1.
asyncLib.map(
array,
(item, callback) => {
// 2.
resolver.resolve(
contextInfo,
context,
item.loader,
resolveContext,
(err, result) => {
// 3.
const parsedResult = this._parseResourceWithoutFragment(result);
// 4.
const resolved = {
loader: parsedResult.path,
options:
item.options === undefined
? parsedResult.query
? parsedResult.query.slice(1)
: undefined
: item.options,
ident: item.options === undefined ? undefined : item.ident
};
// 5.
return callback(null, resolved);
}
);
},
callback // 6.
);
}
}
2.2.1 参数
- contextInfo:构建相关上下文信息,在 factory.create() 调用时传递,经层层传递到达这里;
- context:当前构建的项目的上下文目录;
- array:带解析的路径数组,可以是 loader 的,也可以是自愿模块的;
- resolver:用于解析上面 array 的 resolver,根据解析的物料不同而不同;
- resolverContext:resolver 执行 resolve 时需要的上下文对象,这个对象会伴随 resolver 流水线工作,贯穿整个 resolve 的过程。这个对象中包含三个我们熟悉的对象 file/missing/context dependencies;从这里可以看出后面要用的 file/missing dependencies 是在 resolve 阶段填充的;
- callback:这个 callback 是受理解析结果的回调函数;
下图是个方法在工作时接收到的实参:

当然,在我们的例子中,我们并没有内联的 loader,因此 array 将会是个空数组。。。。(我好想抛开事实不谈。。。。。。🧜♀️🧚♀️)
2.2.2 逻辑
逻辑也是相当的简洁,一共分为 6 个步骤:
- 调用 asyncLib.map 异步并发迭代 array,这个 asyncLib.map(arr, iteratorFun, cb) 接收三个参数,异步的迭代 arr 中的每一项,为它调用 iteratorFun,当所有都迭代结束后调用 cb;
- resolver.resolve() 调用作为迭代函数执行,接收每个待解析的项目进行解析;
- 在得到 resolver.resolve 的结果后调用 _parseResourceWithoutFragment 方法处理一下成标准的 { path, query, resource } 对象;
- 组织 resolved 对象;
- 调用控制 asyncLib.map 的 callback,注意这个 callback 不是 resolveRequestArray 的 callback;
- 在 asyncLib.map 完成对 array 中每一项的迭代后就拿到了所有的结果,此时调用 callback 完成解析工作;
2.3 内联 loader 的优先解析特性
这一点并不是 webpack 显式的告知我们的,而是随着阅读的深入(严格来讲是写作本文时)才发现的特性。内联 loader 无论是在 rquest 的处理还是在 loader 的路径解析层面都有着非常高的优先级。这个设定和 webpack 设计内联 loader 时给予的更高优先级相辉映。
但是,我想说但是,到这里也只是第一层,我想说的是 一致性。这种设计和实现的一致性很值得提倡,这为二次开发的你我,或者阅读这个部分代码的读者来说都是十分容易理解的。
当然,这里并不是最终优先级的组织时机,但是从流程上讲这样处理是非常赞的!
三、总结
本文讲述了 webpack 在 resolve 阶段解析 内联 loader 的细节工作,主要包含了一些内容:
- needCall 这个小方法的意义,以及它背后的设计理念;
- NormalModuleFactory.prototype.resolveRequestArray 方法的实现:并发调用 resolver.reslve 解析,得到解析结果后调用 _parseResourceWithoutFragment 方法后处理解析结果后返回;
- 最后我们分享了 webpack 内部对于 内联loader 从设计到实现的一致性,这一点非常值得推荐;
下面我们开始进入更精彩的环境——常规 loader 的解析以及 loader 顺序的确定还有资源模块的解析!