2024了,你还没学 webpack 的 match-resouce 语法吗?webpack 源码分析:match-re
一、前文回顾
前面 12 篇小作文完成了 webpack 内部的重要对象 Resolver 的实现库—— ehanced-resolve 的全部讲解工作。我们主要从这几方面介绍的这个库:
- 我们介绍了 webpack.config.js.resolve / resolveLoader 相关配置;
- createResolver 中注册 resolver.hooks,值得注意的是,它的 hook 是流水线;
- 介绍实现整个解析过程的 23 个插件的实现;
- 最后全流程梳理,阐述了 resolver 及 enhanced-resolve 在 webpack 宇宙中的重要意义——提供完备和可扩展的路径解析系统,进一步强化 webpack 的可扩展性!
前面的工作都算是 resolve 前的准备工作,现在开始进入 resolve 阶段;
二、开始 resolve
resove 阶段我们从 webpack 内部创建 resolver 开始,而这个过程在 NormalModuleFactory 构造函数中注册的 nmf.hooks.resolve 回调函数:
class NormalModuleFactory extends ModuleFactory {
constructor () {
// ...
this.hooks.factorize.tapAsync(
{
name: "NormalModuleFactory",
stage: 100
},
(resolveData, callback) => {
// 1. 这里触发 resolve 的过程
this.hooks.resolve.callAsync(resolveData, (err, result) => {});
}
);
this.hooks.resolve.tapAsync(
{
name: "NormalModuleFactory",
stage: 100
},
(data, callback) => {
// 【2】. 这里定义了 resolve 的过程
// 获取 loader resolver
const loaderResolver = this.getResolver("loader");
// ...
}
):
}
}
如上面的代码,【2】 处定义了 resolve 的过程,【1】处相当于说 在创建模块的过程中触发了 resolve 的过程。
resolve 过程无非是解析 模块资源路径和 loader 的路径,前面已经讲过 resolver,那么接下来我们先来看看资源和loader 是怎么得来的;
2.1 处理 matchResource 语法
先说说啥是 matchResource,matchResouce 是从 webpack v4 开始引入的一种新的内联请求语法。 其语法:
<match-resource>!=!
将为此请求设置matchResource
。
这东西有什么用呢?webpack 官方文档上如是说:
当
matchResource
被设置时,它将会被用作匹配module.rules
而不是源文件。如果需要对资源应用进一步的 loader,或者需要更改模块类型
听起来还是云里雾里的是吧,其实很简单就是更改原有的 request 要使用的 loader!上面提到了一个很重要的概念 module.rules
,这个东西可以粗暴的理解为 webpack.config.js.module.rules,但是在此处,它是经过 webpack 格式化之后的了。module.rules 大家熟悉的很,设置那些 模块要应用什么 loader 的配置项。
现在我们看 webpack 怎么处理这种场景的:
// 1.
let requestWithoutMatchResource = request;
// 2.
const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request); // /^([^!]+)!=!/
// 3.
if (matchResourceMatch) {
// 4.
let matchResource = matchResourceMatch[1];
// 5.
if (matchResource.charCodeAt(0) === 46) {
// 46 === ".", 47 === "/"
const secondChar = matchResource.charCodeAt(1);
// 6.
if (
secondChar === 47 ||
(secondChar === 46 && matchResource.charCodeAt(2) === 47)
) {
// 7.
// if matchResources startsWith ../ or ./
matchResource = join(this.fs, context, matchResource);
}
}
// 8.
matchResourceData = {
resource: matchResource,
...cacheParseResource(matchResource)
};
// 9.
requestWithoutMatchResource = request.slice(
matchResourceMatch[0].length
);
}
结合上面的代码,这个过程分为 9 步骤:
- 缓存当前 request 到 requestWithoutMatchResource 变量(不带 match-resource 的 request);
- 尝试使用 MATCH_RESOURCE_REGEX 正则捕获 matchResource(正则长这样 /^([^!]+)!=!/),将捕获的内容存于 matchResourceMatch 常量;
- 判断 matchResourceMatch 如果不是 null,就说明命中了 matchReasource 场景;
- 命中 match-resource 之后,从 matchResouce 获取第一个分组的内容换场到 matchResouce 常量;
- 接着需要进一步判断这个 match-resource 是不是个相对路径所以有了以下判断:判断第一个字符的 code 是不是 46(即
.
); - 如果第一个是 46 就判断第二个字符 code 是不是 47(即
/
)(其实就是判断前两个字符是不是./
); - 此时说明命中 match-resoucr 带有相对路径,此时需要调用 join 将相对路径拼接成绝对路径;
- 组织 matchResourceData;
- 更新 requestWithoutMatchResource 值为 除了 matchResource 之后的值;
2.2 获取 rquest 上的 loader 和资源模块路径
在 webpack 中,除了我们常见的通过 webpack.config.js.module.rules 指定 loader 以外,webpack 还支持一种内联的方式生命 loader。
具体操作则是通过 import(或等同于 import 的语句)在资源路径中加入 loader,这里摘抄一个 webpack 文档中的例子:
import Styles from 'style-loader!css-loader?modules!./styles.css';
!
用作多个内联 loader
的分隔符,同时也是 loader 和 资源路径的分隔符;
下面我们看看 webpack 在这里怎么处理的:
// 1.
const firstChar = requestWithoutMatchResource.charCodeAt(0);
// 2.
const secondChar = requestWithoutMatchResource.charCodeAt(1);
// 3.
noPreAutoLoaders = firstChar === 45 && secondChar === 33; // startsWith "-!"
// 4.
noAutoLoaders = noPreAutoLoaders || firstChar === 33; // startsWith "!"
// 5.
noPrePostAutoLoaders = firstChar === 33 && secondChar === 33; // startsWith "!!";
// 6.
const rawElements = requestWithoutMatchResource
.slice(
noPreAutoLoaders || noPrePostAutoLoaders
? 2
: noAutoLoaders
? 1
: 0
)
.split(/!+/);
// 7.
unresolvedResource = rawElements.pop();
// 8.
elements = rawElements.map(el => {
const { path, query } = cachedParseResourceWithoutFragment(el);
return {
loader: path,
options: query ? query.slice(1) : undefined
};
});
- 获取 requestWithoutMatchResource 的第一个字符存于 firstChar 常量;
- 获取 requestWithoutMatchResource 的第二个字符存于 secondChar 常量;
- 判断前两个字符是
-!
,将结果存于 noPreAutoLoaders;这个东西有什么用呢?如果是,则这个东西用于禁用已配置的 preLoader 和 loader,但是不禁用 postLoaders; - 判断前两个字符是不是
!
,将结果存于 noAutoLoaders;这个玩意儿可以禁用已经配置的 loader(normal laoder); - 判断前两个字符是不是
!!
,将结果存于 noPrePostAutoLoaders,这个玩意儿可以禁用所有配置的 loader,无论 preLoader、loader 还是 postLoader。 - 开始获取原始资源路径,别看这个过程挺唬人,其实就是简单的字符串截取。那后面的那一堆三元运算在干啥?其实是确定截取的起点,根据前面判断的前两个字符是不是
-!/!!/!
这些决定是从第三个还是以第二个还是第一个开始截取,最终结果再用!
分隔(.split(/!+/)
),这样分割之后得到的数组长这样:[loaderA, loaderB, loaderC, ..., 资源路径]
,最后把这个数组保存到 rawElements; - 结合 6. 已知 rawElements 最后一个是模块资源路径,rawElements.pop() 就是取得资源路径并存于 unresolvedResource;记住这个变量,这个是我们下面要解析的资源模块!
- 格式化 rawElements 得到 elements,格式化的目的主要是解析 loader 声明中是传入的参数
loaderA?some=test&fun1=aaa!loaderB!./src/inex.js
,这种 ?some=test&fun1=aaa 就是 query;
经过这一步,我们已经得到了 request 上的内联 loader(elements)及待解析的 模块资源路径 (unresolvedResource);
三、总结
本文主要讲述了 webpack 的 resolve 阶段在得到 loaderResolver 之后的解析 request 的工作,这部分工作主要包含两方面:
- 处理 match-resource 语法(!=!),这一步主要是获取 matchResourceData,并且将 matchResource 从原有的 request 上移除以便进行后续的解析工作。
这种语法是 webpack v4 之后支持的一种新语法,用于在编译中暴力修改原有 request 应该使用的 loader。值得注意的是,这种语法在业务中不要用,这个东西有点危险!
- 解析 requestWithoutMatchResource,分析 request 开头的字符是不是
!/-!/!!
来决定是不是禁用调用配置中的loader,最后将 loader 和资源路径分离;- 2.1 ! 用于禁用常规 loader;
- 2.2 -! 禁用 preLoader 和 loader,但是不禁用 postLoader;
- 2.3 !! pre/post/normalLoader 都禁用;
经过这两个步骤后我们得到 request 上带有的 内联 loader
和 模块资源路径;
注意这里的措辞:我这里说的不是 loader
而是 内联 loader
!为什么这么说,这是因为截止到目前为止,我们只是处理了 request,只是得到 request 上带的内联部分。
正如你所知,在 webpack.config.js 中配置的 module.rules 中还配置了其他的 loader。
转载自:https://juejin.cn/post/7416911270660128803