Loader 的调用链

79 篇文章 0 订阅
79 篇文章 0 订阅

一、前言

在上一篇 《手把手教你实现 Loader》 中已经介绍了 loader 的基础知识以及如何自定义 loader。这篇文章主要聊聊 loader 的调用链,从这篇文章中,你将了解到以下内容:

  • loader 的 normal & pitch 阶段
  • loader 在不同阶段的执行顺序
  • pitch 的应用(style-loader)

相关代码基于上篇文章,建议优先阅读上一篇。文章如有误,欢迎指出,大家一起学习交流~。 项目地址,期待大家的一键三连 💗

二、loader的两大阶段

一个 loader 可以分为两个阶段: 一个是 normal 阶段 ,另一个是 pitch 阶段。不同阶段对应的代码如下:

pitch 阶段的执行时期会早于 normal 阶段。那么具体的执行顺序是怎么样的呢?下面我们分开来聊一聊。

三、loader 的 normal 阶段

在开始之前,我们先来聊聊 enforce 这个属性 ,在配置文件中,可以通过 enforce 属性定义 loader 的类型, enforce 属性的取值为 pre || post ,没有则视为普通( normal)的 loader 。即官方提到的 pre-loaderpost-loadernormal-Loader, 顾名思义,它们执行顺序为 pre → normal → post

理论不如实战,我们还是基于上一个项目来实战一下。在 xq-loader 的文件夹下创建三个文件 a-loader.jsb-loader.jsc-loader.js

分别在每个文件中填充内容,并进行打印。

接着在配置文件中,配置着三个loader。为了方便理解 loader 的执行顺序,减少项目中其他 loader 的影响,我们用这三个 loader 来处理 txt 文件,并使用 enforce 属性进行标识 。因 webpack 不能识别 txt 文件,所以我们保留之前的 my-async-loader 调用。

配置完毕后, pnpm run build 打包之后,可以看到控制台的打印如下:

可以验证前面的执行顺序 pre → normal → post ,那么对于未使用 enforce 字段标识的 b-loadermy-async-loader 的执行顺序是啥呢?咱们可以记个口诀,对于相同类型的 loader 的执行顺序是自下而上,从左到右。

为了更好的理解这个口诀,我们再增加一个普通的 d-loader.js, 调整一下配置:

此时我们将三个普通的 loader 抽象出来, 可以将它们理解为一个二维数组,大致是这样:

my-async-loader
d-loaderb-loader

套用一下口诀,执行顺序应该是 b-loader → d-loader → my-async-loader ,打包编译下:

没错,符合预期,看起来这个口诀还是挺管用的呢!

除了 enforce 字段可以决定 loader 的执行顺序,还有一种内联 import 的写法也可以改变loader 的执行顺序,让我们来尝试下这种内联写法。调整下 d-loader 的位置,先将它从配置文件中移除,然后在引入.txt 的 index.js 文件中通过 import 的方式引入它。多个 loader 之间的连接可以通过 来进行分割。

我们再来看一看增加了这种行内(inline)写法之后的 loader 执行顺序如下,即:pre → normal → inline → post 。

行内写法也可以通过添加前缀的方式来改变 loader 的执行顺序,一共有 3 种前缀的语法:

  • ! : 忽略已配置的 normol-loader
  • -!:忽略已配置的 pre-oader、normol-loader
  • !!:忽略已配置的 (pre、normol、post) loader

让我们来实践一下 ,先整理下 webpack.config.js 中的配置。为了更好的测试,将 my-async-loader放到行内写法中,保留 a-loaderb-loaderc-loader

执行结果:a-loader → my-async-loader → d-loader → c-loader

-! 前缀

 

执行结果:my-async-loader → d-loader → c-loader

3.1 loader-normal 的参数

normal 阶段接收三个参数:

  • source

    表示需要处理的源文件的内容。对于第一个loader 来说,source 是源文件,对于后续的 loader 来说是前一个loader 执行之后的结果。

  • sourceMap

    可选参数,代码的sourceMap

  • data

    可选参数,其他需要在loader 链中传递的信息。比如这个posthtml-loader 使用了data 来传递ast 信息。

到这里我们就聊完了loader的 normal 阶段的执行顺序,接下来我们来了解下 loader 的 pitch 阶段的执行顺序。

四、loader 的 pitch 阶段

对于一个 loader ,可以用 module.exports 来导出一个函数外(normal 阶段), 也可以用 module.exports.pitch 来导出(pitch 阶段)一个函数。前面说过对于 normal 阶段来说同一类型的 loader 可以套用口诀,自下而上,从右到左,不同类型遵循 pre → normal → inline → post 。 那么对于 pitch 阶段来说反过来即可。

我们来实践一下,先更改下配置,每个 loader 中导出 pitch 函数:

最终打印的结果如下:

可以看到,pitch 阶段先执行, normal 阶段后执行,pitch 阶段正好和 normal 阶段相反。

4.1 pitch 的熔断作用

上述的例子中,在 ptich 阶段我们都没有任何的返回操作,那如果在 pitch 阶段有返回值,loader 的执行顺序是什么样呢?

loader 的调用过程:

如果 pitch 过程中有非 undefined 的返回值时,将熔断 loader 的调用链,跳过后续的 loader ,将结果传递给前一个loader 。

在 d-loader 的pitch 阶段返回一个值:

执行结果如下,直接熔断后续 loader 的调用:

常见的 style-loader 、vue-loader 都用到了pitch 阶段的熔断作用来实现。

4.2 loader - pitch 的参数

ptich 方法接受3个参数:

  • remainingRequest

    表示剩余未处理的loader的绝对路径和资源文件的绝对路径 用 !分割 组合成的字符串。

    对于上面的调用链图来说,以 my-async-loader 为例,remainRequest 表示的是右侧这部分的 loader的绝对路径+资源文件的绝对路径,即:xxx/d-loader.js!xxx/b-loader.js!xxx/a-loader.js!xxx/.txt

previousRequest

表示已经处理的loader的绝对路径用 !分割 组合成的字符串。

还是以 my-async-loader 为例,previousRequest的是左侧这一部分的 loader 的绝对路径,即: xxx/c-loader.js

data

normal 阶段与pitch阶段之间的数据交互可以用 data 对象来传递,默认是一个空对象 {}。

当在一个 loader 的 pitch 阶段中对其 data 参数做处理后,在 normal 阶段可以通过 this.data 进行获取。

4.3 pitch的应用 — style-loader

一般情况下处理 css/less 文件的 loader 调用链为 [style-loader、css-loader, less-loader]

  • less-loader: 将 less 转换为标准 css。
  • css-loader : 将 css 转换为 js 模块。
  • style-loader : 将 js 模块以 link 、style 标签等方式挂在到 html 中。

实际上,style-loader 的作用只是将 css 如何挂在到 html 中, 其他的并不关心。style-loader 的核心代码如下:

injectType 表示将 css 以什么形式插入到 DOM 中,不同的 type 处理方式不同, 我们直接看 default 部分,比较符合日常的使用,即使用 <style> 标签插入到 DOM 中。这一部分关键点可以分为4步。

第二步:导入一个 insertStyleElement方法,用于创建 style 空标签。

js 代码解读复制代码// 导入空 style 标签
${getImportInsertStyleElementCode(esModule, this)}
// 实际返回
import insertStyleElement from "!../node_modules/.pnpm/style-loader@3.3.1_webpack@5.73.0/node_modules/style-loader/dist/runtime/insertStyleElement.js";

// insertStyleElement 方法如下
function insertStyleElement(options) {
  const element = document.createElement("style");
  options.setAttributes(element, options.attributes);
  options.insert(element, options.options);
  return element;


第三步:导入 css-loader 处理的结果,是一个被 webpack 处理好的 module。

js 代码解读复制代码 // 导入 css 结果
 ${getImportStyleContentCode(esModule, this, request)}
 // 实际返回
 import content, * as namedExport from "!!../node_modules/.pnpm/css-loader@6.7.1_webpack@5.73.0/node_modules/css-loader/dist/cjs.js!./xq-loader/sprite-loader.js!./index.css"

这里 getImportStyleContentCode 方法调用上下文的 this.utils api 中的 contextify 方法将 request 路径 (即pitch 方法的第一个参数 remainingRequest)转换为一个新的请求路径(即一个相对路径)。
js 代码解读复制代码function getImportStyleContentCode(esModule, loaderContext, request) {
  const modulePath = stringifyRequest(loaderContext, `!!${request}`);

  return esModule
    ? `import content, * as namedExport from ${modulePath};`
    : `var content = require(${modulePath});`;
}
// stringifyRequest 方法返回的内容
loaderContext.utils.contextify(loaderContext.context, request) => "../node_modules/.pnpm/css-loader@6.7.1_webpack@5.73.0/node_modules/css-loader/dist/cjs.js!./xq-loader/sprite-loader.js!./index.css"

注意这里的 !! 符号,即忽略已经配置过的 loader ,避免循环执行。webpack 执行到这里时,会递归执行这个行内loader 最终返回一个 module(即这里的conent),可供 runtime 阶段直接使用。(这里是按照项目中配置的loader来,所以会有一个 xq-loader/sprite-loader.js)
js 代码解读复制代码import content, * as namedExport from "!!../node_modules/.pnpm/css-loader@6.7.1_webpack@5.73.0/node_modules/css-loader/dist/cjs.js!./xq-loader/sprite-loader.js!./index.css"


第四步:runtime阶段,将前面导入的 css内容插入到style 标签中。

js 代码解读复制代码var update = API(content, options);

以上就是 style-loader 的工作原理。假如这里 style-loader 设计为一个 normal loader,按照执行顺序,style-loader 需要额外处理 css-loader 返回的 js 脚本内容,提取需要的样式内容,这样无疑需要写一堆处理逻辑,设计成 pitch loader 只需要关注如何将 css 内容插入到 DOM 中即可,其他的交给 webpack 处理。将这一部分转换为流程图更好理解。

执行 pitch 方法,返回一段 js 脚本给 webpack ,后续 loader 不再执行

  • webpack 继续解析构建 style-loader 返回的内容,遇到 inline-loader 之后

五、总结

  • loader的两个阶段:对于一个 loader 来说,都有自己的 normal 方法和 pitch 方法。
  • loader 的调用链:对于多个 loader 的调用链即先从低到高(post → pre)依次调用 pitch 方法,再依次从高到低 (pre → post)调用各自的 normal 方法。
  • pitch 的熔断作用:pitch 方法中如有非undefined的返回,则会熔断后续loader的执行。style-loader 、vue-loader 都巧妙的利用了 pitch 的熔断作用。

原文链接:https://juejin.cn/post/7398748051878166568

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值