一、前文回顾
前文中我们详细讨论模块创建的一个分支流程即如何获取当前的模块需要应用到 loaders 结果集。这个过程呢呢是有 RuleSetCompiler 这个类型来实现的。
上文详细介绍了 RuleSetCompiler 类型的工作原理,回顾整个调用过程:
- 首先 NMF 实例创建了 RuleSetCompiler 的实例;
- 接着调用了 ruleSetCompiler.compileRules 编译 rules,得到带有 exec 方法的对象;
- 在有了模块路径后调用上一步得到的 exec 方法获取当前模块路径需要应用的 loader;
回顾整个过程:
- RuleSetCompiler 把 webpack 内置的和通过 webpack.config.js.module.rules 配置而来的规则进行编译,得到标准的 ruleSet 对象,其中 { exec: () => { ... }, references };
- 在调用 nmf.ruleSet.exec 方法时,内部会遍历前面注册的各种 rule 最终把命中的 rule 添加到 effects 中,而这个 effect 就是最终被应用的 loader;
今天我们的视线回到 webpack 创建模块的过程中,接上文我们获取到了 loader 集合后面的事情。
二、ruleSet.exec 的返回值
以下是 exec 的调用过程:
js
const result = this.ruleSet.exec({
resource: resourceDataForRules.path,
realResource: resourceData.path,
resourceQuery: resourceDataForRules.query,
resourceFragment: resourceDataForRules.fragment,
scheme,
assertions,
mimetype: matchResourceData
? ""
: resourceData.data.mimetype || "",
dependency: dependencyType,
descriptionData: matchResourceData
? undefined
: resourceData.data.descriptionFileData,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler,
issuerLayer: contextInfo.issuerLayer || ""
});
2.1 重要参数
这里有几个关键的参数需要关注:
- resource: resourceDataForRules.path,在非 matchResource 模式下就是模块的真实的资源路径,如果是 matchResource 模式时,则是 matchResource 声明的目标资源路径,这个有点特殊,比如 你有一个 vue 的组件,a.vue,在 a.vue 被编译时,样式块就需要被应用一些 loader,比如 stylus-loader,此时就可以通过 matchResouce 返回这样一个 a.vue.stylus 这样的 资源模块;对应的也就得到这样一个虚拟的模块 path;
- realResource: resourceData.path,这里就是资源路径的真实路径;
2.2 result
nmf.ruleSet.exec 的返回值是一个数组对象,其中的每一项包含两个属性即 { type, value },以我们的例子而言:
json
const result = [
{
"type": "use",
"value": {
"loader": "babel-loader",
"options": {
"presets": [
"@babel/preset-env"
],
"plugins": []
},
"ident": "ruleSet[1].rules[0].use[0]"
}
}
]
这里这个很简单,就是我们声明的 babel-loader,现在我们具体分析一下这个结构:
- type:类型,即 loader 的类型,类型分为三种类型,这个和 webpack 的配置中的 rule.enforce 结合,这个东西后面用来组织 webpack 的 loader 的执行顺序;
- 1.1 "use": 常规 loader,也就是 normal loader;
- 1.2 "use-pre":前置 loader;
- 1.3 "use-post":后置 laoder;
- value:这个是值类型,这个 value 用于描述当前这个 loader 的信息,以我们上面例子中来看这里面的属性含义:
- 2.1 loader:loader 的名称;
- 2.2 options:传递给 loader 执行的选项配置信息,这里面的东西不是固定的,因 loader 而异,如果你是 loader 开发者,这里面的东西可以自定义;
- 2.3 ident:loader 的标识符,这个玩意儿一般是由 webpack 在编译 rule 时自己生成的;
2.3 给 loader 分类
有了上面的 loader 之后并不能进入到执行 loader 的过程,在执行之前还有很多路要走!
有了 loader 需要先根据前面的结果中的类型 type 进行分类:
js
for (const r of result) {
if (r.type === "use") {
if (!noAutoLoaders && !noPrePostAutoLoaders) {
useLoaders.push(r.value);
}
} else if (r.type === "use-post") {
if (!noPrePostAutoLoaders) {
useLoadersPost.push(r.value);
}
} else if (r.type === "use-pre") {
if (!noPreAutoLoaders && !noPrePostAutoLoaders) {
useLoadersPre.push(r.value);
}
} else if (
typeof r.value === "object" &&
r.value !== null &&
typeof settings[r.type] === "object" &&
settings[r.type] !== null
) {
settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
} else {
settings[r.type] = r.value;
}
}
这个分类的过程其实已经简述过了,其实很简单就是遍历 result 这个结果集合,然后根据每一项的 type 分包放到不同的数组中:
- type === 'use':放到 useLoaders 数组中;
- type === 'use-post':放到 useLoaderPost 数组中;
- type === 'use-pre':放到 useLoadersPre 数组中;
三、解析 loaders
虽然上面已经得到了具体需要应用的 loaders,但是这些 loader 都只是一个名字而已,其具体的模块路径还不知道。因此在经历上面的排序和分类之后,下面的工作就要进入到针对 loader 的解析工作:
3.1 声明 continueCallback
js
const continueCallback = needCalls(3, err => { /* ... */ });
这个 continueCallback 方法和外面的 continueCallback 名字一样,但是方法作用不同。这个 continueCallback 需要在 3 次调用后执行声明时传入的 callback 逻辑。
这 3 次调用后的 callback 作用是最后用于组织有关 nmf.hooks.resolve 这个钩子中的最终结果用的。具体的执行细节暂时先不过多展开咯!
3.2 解析后置 loader
后置 loader 就是前面提及的 useLoaderPost 数组了:
js
this.resolveRequestArray(
contextInfo,
this.context,
useLoadersPost, // 后置 loader 数组
loaderResolver,
resolveContext,
(err, result) => {
postLoaders = result;
continueCallback(err);
}
);
这里都是大家熟悉的戏码了,调用 this.resolveRequestArray,解析数组中的 request;这里第三个参数传入的就是 useLoadersPost;
完成解析后再回调用把 结果 result 赋值给 postLoaders,并调用 continueCallback 计数!
3.2 解析常规 loader
接着用调用 this.resolveRequestArray 解析普通的 loader,原理不再赘述
js
this.resolveRequestArray(
contextInfo,
this.context,
useLoaders, // 常规 loader
loaderResolver,
resolveContext,
(err, result) => {
normalLoaders = result;
continueCallback(err);
}
);
完成解析后再回调用把 结果 result 赋值给 normalLoaders,并调用 continueCallback 计数!
3.3 解析前置 loader
解析前置 loader
js
this.resolveRequestArray(
contextInfo,
this.context,
useLoadersPre, // 前置的 loader
loaderResolver,
resolveContext,
(err, result) => {
preLoaders = result;
continueCallback(err);
}
);
完成解析后再回调用把 结果 result 赋值给 normalLoaders,并调用 continueCallback 计数。
3.4 continueCallback 的 callabck 预告
这里算一个简单的总结了,continueCallback 要求计数 3 次后调用,解析后置 loader、常规 loader 解析、前置 loader 解析,三次已经到了。
并且这里我们已经获得了所有的 loader 了,下面我们进入 nmf.hooks.resolve 的收尾工作!
四、nmf.hooks.resolve 的收尾
这个收尾工作交由 continueCallback 完成,下面我们看看它都做了哪些工作:
js
err => {
// 1.
if (err) {
return callback(err);
}
// 2.
const allLoaders = postLoaders;
if (matchResourceData === undefined) {
for (const loader of loaders) allLoaders.push(loader);
for (const loader of normalLoaders) allLoaders.push(loader);
} else {
for (const loader of normalLoaders) allLoaders.push(loader);
for (const loader of loaders) allLoaders.push(loader);
}
// 3.
for (const loader of preLoaders) allLoaders.push(loader);
// 4.
let type = settings.type;
const resolveOptions = settings.resolve;
const layer = settings.layer;
if (layer !== undefined && !layers) {
return callback(
new Error(
"'Rule.layer' is only allowed when 'experiments.layers' is enabled"
)
);
}
// 5.
try {
Object.assign(data.createData, {
layer:
layer === undefined ? contextInfo.issuerLayer || null : layer,
request: stringifyLoadersAndResource(
allLoaders,
resourceData.resource
),
userRequest,
rawRequest: request,
loaders: allLoaders,
resource: resourceData.resource,
context:
resourceData.context || getContext(resourceData.resource),
matchResource: matchResourceData
? matchResourceData.resource
: undefined,
resourceResolveData: resourceData.data,
settings,
type,
parser: this.getParser(type, settings.parser),
parserOptions: settings.parser,
generator: this.getGenerator(type, settings.generator),
generatorOptions: settings.generator,
resolveOptions
});
} catch (e) {
return callback(e);
}
// 6.
callback();
}
整个方法我们分为 6 个步骤:
-
处理错误,一旦有错则调用 callback 传入 err 并终止流程;
-
处理 loader 的排序,因为 loader 最终是倒着执行的,但是组织顺序的时候 postLoader 就放在数组的最前面的,这也是声明 allLoaders = postLoaders 的原因,整个过程如下:
- 2.1 处理不使用 matchResource 的情况,此时先添加 loaders 中的行内 loader 到 allLoaders 中;
- 2.2 处理完行内 loader 再把 normalLoader 中的常规 loader 添加到 allLoaders 数组;
- 2.3 接着处理命中 matchResource 语法的情况,此时需要先添加常规 loaders 到 allLoaders 中;
- 2.4 在 matchResource 语法中,matchResource 中的行内 loader 要优先于常规 loader 执行,因此要放到常规loader 后面添加(注意,这个是倒着执行的顺序,先添加的后执行!!!!)
-
最后则是添加 preLoader 到 allLoaders 数组中(这里还是因为 allLoaders 中的 loader 是倒着执行的,最后添加的将来到了执行的时候最先执行);
-
处理 type 和 layer,这里不再多说;
-
最后组织 data.createData 对象,这里面包含了 nmf.hooks.resolve 的最终工作产出,这里有些数据后面要用需要注意一下!
-
调用 callback 并传入 data,这个 callback 会结束 nmf.hooks.resolve 的执行,回到 nmf.hooks.factory 中;
五、总结
本文回到了 NMF 中创建模块的重要流程 ------ hooks.resolve 的流程中,核心点还是关于 loader 的组织,期间讨论了以下重点内容:
- nmf.ruleSet.exec 方法的工作原理,重点解析了其参数、返回值、以及其中的 loader 分类的过程;
- 接着讨论了解析 loader 的过程,按照 loader 的类型即 use/use-post/use-pre 进行分开解析;
- 在解析完成后会执行 continueCallback:
- 3.1. 这个过程主要是处理 loader 的顺序问题,与顺序相关的因素处理 loader 的类型外,还有一个 matchResource 语法,它主要影响行内 loader 的是在常规 loader 还是之后;
- 3.2 最后则是组织 data.createData 为创建模块和构建模块备用;
- 3.3 调用 callback 把执行权限重新交还到 nmf.hooks.factory 继续模块的创建工作;