likes
comments
collection
share

2024了,你还没学 webpack 的 match-resouce 语法吗?webpack 源码分析:match-re

作者站长头像
站长
· 阅读数 35

一、前文回顾

前面 12 篇小作文完成了 webpack 内部的重要对象 Resolver 的实现库—— ehanced-resolve 的全部讲解工作。我们主要从这几方面介绍的这个库:

  1. 我们介绍了 webpack.config.js.resolve / resolveLoader 相关配置;
  2. createResolver 中注册 resolver.hooks,值得注意的是,它的 hook 是流水线;
  3. 介绍实现整个解析过程的 23 个插件的实现;
  4. 最后全流程梳理,阐述了 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 步骤:

  1. 缓存当前 request 到 requestWithoutMatchResource 变量(不带 match-resource 的 request);
  2. 尝试使用 MATCH_RESOURCE_REGEX 正则捕获 matchResource(正则长这样 /^([^!]+)!=!/),将捕获的内容存于 matchResourceMatch 常量;
  3. 判断 matchResourceMatch 如果不是 null,就说明命中了 matchReasource 场景;
  4. 命中 match-resource 之后,从 matchResouce 获取第一个分组的内容换场到 matchResouce 常量;
  5. 接着需要进一步判断这个 match-resource 是不是个相对路径所以有了以下判断:判断第一个字符的 code 是不是 46(即 .);
  6. 如果第一个是 46 就判断第二个字符 code 是不是 47(即 /)(其实就是判断前两个字符是不是 ./);
  7. 此时说明命中 match-resoucr 带有相对路径,此时需要调用 join 将相对路径拼接成绝对路径;
  8. 组织 matchResourceData;
  9. 更新 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
    };
});

  1. 获取 requestWithoutMatchResource 的第一个字符存于 firstChar 常量;
  2. 获取 requestWithoutMatchResource 的第二个字符存于 secondChar 常量;
  3. 判断前两个字符是 -!,将结果存于 noPreAutoLoaders;这个东西有什么用呢?如果是,则这个东西用于禁用已配置的 preLoader 和 loader,但是不禁用 postLoaders;
  4. 判断前两个字符是不是 !,将结果存于 noAutoLoaders;这个玩意儿可以禁用已经配置的 loader(normal laoder);
  5. 判断前两个字符是不是 !!,将结果存于 noPrePostAutoLoaders,这个玩意儿可以禁用所有配置的 loader,无论 preLoader、loader 还是 postLoader。
  6. 开始获取原始资源路径,别看这个过程挺唬人,其实就是简单的字符串截取。那后面的那一堆三元运算在干啥?其实是确定截取的起点,根据前面判断的前两个字符是不是 -!/!!/! 这些决定是从第三个还是以第二个还是第一个开始截取,最终结果再用 ! 分隔(.split(/!+/)),这样分割之后得到的数组长这样:[loaderA, loaderB, loaderC, ..., 资源路径],最后把这个数组保存到 rawElements;
  7. 结合 6. 已知 rawElements 最后一个是模块资源路径,rawElements.pop() 就是取得资源路径并存于 unresolvedResource;记住这个变量,这个是我们下面要解析的资源模块!
  8. 格式化 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 的工作,这部分工作主要包含两方面:

  1. 处理 match-resource 语法(!=!),这一步主要是获取 matchResourceData,并且将 matchResource 从原有的 request 上移除以便进行后续的解析工作。这种语法是 webpack v4 之后支持的一种新语法,用于在编译中暴力修改原有 request 应该使用的 loader。值得注意的是,这种语法在业务中不要用,这个东西有点危险!
  2. 解析 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
评论
请登录