理解 loader 的工作流
分类
loader 分成哪几类?如果指定类型?
loader 的叠加顺序 = post(后置)+inline(内联)+normal(正常)+pre(前置)。Rule.enforce 可能的值有:"pre" | "post",指定 loader 种类。没有值表示是普通 loader。还有一个额外的种类"行内 loader",loader 被应用在 import/require 行内。
module: {
rules: [
{
test: /\.css$/,
use: [
// 直接这样配置就是normal
'css-loader'
]
},
{
test: /\.css$/,
enforce: 'pre',
use: [
// 这样是前置pre
'style-loader'
]
},
{
test: /\.less$/,
enforce: 'post',
use: [
// 这样是后置post
'less-loader'
]
}
]
}
// 这样是内联inline
import Styles from 'style-loader!css-loader?modules!./styles.css';
特殊配置
加载模块前的特殊符号是如何工作的?
符号 | 变量 | 含义 |
-! | noPreAutoLoaders | 不要前置和普通 loader |
! | noAutoLoaders | 不要普通loader |
!! | noPrePostAutoLoaders | 不要前后置和普通 loader,只要内联 loader |
内联方式,可以在 import 语句或任何 与 "import" 方法同等的引用方式 中指定 loader。使用 !
将资源中的 loader 分开。每个部分都会相对于当前目录解析。
import Styles from 'style-loader!css-loader?modules!./styles.css';
通过为内联 import 语句添加前缀,可以覆盖 配置 中的所有 loader, preLoader 和 postLoader:
使用 !
前缀,将禁用所有已配置的 normal loader(普通 loader)
import Styles from '!style-loader!css-loader?modules!./styles.css';
使用 !!
前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader)
import Styles from '!!style-loader!css-loader?modules!./styles.css';
使用 -!
前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders
import Styles from '-!style-loader!css-loader?modules!./styles.css';
选项可以传递查询参数,例如 ?key=value&foo=bar
,或者一个 JSON 对象,例如 ?{"key":"value","foo":"bar"}
。
组合
这些不同的loader是如何组合的?
还有一个额外的种类"行内 loader",loader 被应用在 import/require 行内。
所有一个接一个地进入的 loader,都有两个阶段:
1、Pitching 阶段: loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。
2、Normal 阶段: loader 上的 常规方法,按照 前置(pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。模块源码的转换, 发生在这个阶段。
loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。
loader 可以通过 request 添加或者禁用内联前缀,这将影响到 pitch 和执行的顺序。
对于以下 use 配置:
module.exports = {
//...
module: {
rules: [
{
//...
use: ['a-loader', 'b-loader', 'c-loader'],
},
],
},
};
将会发生这些步骤:
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
那么,为什么 loader 可以利用 "pitching" 阶段呢?
首先,传递给 pitch 方法的 data,在执行阶段也会暴露在 this.data 之下,并且可以用于在循环时,捕获并共享前面的信息。
module.exports = function (content) {
return someSyncOperation(content, this.data.value);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
data.value = 42;
};
其次,如果某个 loader 在 pitch 方法中给出一个结果,那么这个过程会回过身来,并跳过剩下的 loader。在我们上面的例子中,如果 b-loader 的 pitch 方法返回了一些东西:
module.exports = function (content) {
return someSyncOperation(content);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
if (someCondition()) {
return (
'module.exports = require(' +
JSON.stringify('-!' + remainingRequest) +
');'
);
}
};
上面的步骤将被缩短为:
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
loader-runner
所谓 loader 只是一个导出为函数的 JavaScript 模块。它接收上一个 loader 产生的结果或者资源文件(resource file)作为入参。也可以用多个 loader 函数组成 loader chain。compiler 需要得到最后一个 loader 产生的处理结果。这个处理结果应该是 String 或者 Buffer(被转换为一个 string)。loader-runner 是一个执行 loader chain 的模块。
const { runLoaders } = require("loader-runner")
const fs = require("fs")
runLoaders({
resource: "/Users/didi/workspace/2021/webpack5/source.js?query", // 你要加载的资源
loaders: ["/Users/didi/workspace/2021/webpack5/loader.js?query"], // 需要处理的loader
context: { minimize: true }, // 保存一些状态和值
readResource: fs.readFile.bind(fs)
}, function(err, result) {
console.log(err, '运行错误')
console.log(result, '运行结果')
})
经过 loader-runner 处理的结果为:
{
result: [ "console.log('hello world')// hello world" ],
resourceBuffer:
63 6f 6e 73 6f 6c 65 2e 6c 6f 67 28 27 68 65 6c 6c 6f 20 77 6f 72 6c 64 27 29>,cacheable: true,
fileDependencies: [ '/Users/didi/workspace/2021/webpack5/source.js' ],
contextDependencies: [],
missingDependencies: []
}