一、前言
在上一篇 《手把手教你实现 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-loader
、post-loader
、normal-Loader
, 顾名思义,它们执行顺序为 pre → normal → post
。
理论不如实战,我们还是基于上一个项目来实战一下。在 xq-loader 的文件夹下创建三个文件 a-loader.js
、b-loader.js
、 c-loader.js
。
分别在每个文件中填充内容,并进行打印。
接着在配置文件中,配置着三个loader。为了方便理解 loader 的执行顺序,减少项目中其他 loader 的影响,我们用这三个 loader 来处理 txt 文件,并使用 enforce
属性进行标识 。因 webpack 不能识别 txt 文件,所以我们保留之前的 my-async-loader
调用。
配置完毕后, pnpm run build
打包之后,可以看到控制台的打印如下:
可以验证前面的执行顺序 pre → normal → post ,那么对于未使用
enforce
字段标识的 b-loader
和 my-async-loader
的执行顺序是啥呢?咱们可以记个口诀,对于相同类型的 loader 的执行顺序是自下而上,从左到右。
为了更好的理解这个口诀,我们再增加一个普通的 d-loader.js
, 调整一下配置:
此时我们将三个普通的 loader 抽象出来, 可以将它们理解为一个二维数组,大致是这样:
my-async-loader | |
d-loader | b-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-loader
、b-loader
、c-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步。
- 第一步:导入 API 方法。 injectStylesIntoStyleTag 主要是提供将 css 样式插入到 style 标签的 api。
第二步:导入一个 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