loader 运行的总体流程

webpack定义了一个属性 enforce,取值有 pre(为pre loader)、post(为post loader),如果没有值值则默认为(normal loader)。所以loader在webpack中有4种:normal,inline,pre,post。
loader执行的时候是基于loader runner库执行的,下面我们来简单模拟一下loader的执行过程。
新建如下八个loader文件:
loaders/inline-loader1.js
function loader(source) {
console.log("inline1");
return source + "//inline1";
}
module.exports = loader;
loaders/inline-loader2.js
function loader(source) {
console.log("inline2");
return source + "//inline2";
}
module.exports = loader;
loaders/normal-loader1.js
function loader(source) {
console.log("normal1");
return source + "//normal1";
}
module.exports = loader;
loaders/normal-loader2.js
function loader(source) {
console.log("normal2");
return source + "//normal2";
}
module.exports = loader;
loaders/post-loader1.js
function loader(source) {
console.log("post1");
return source + "//post1";
}
module.exports = loader;
loader/post-loader2.js
function loader(source) {
console.log("post2");
return source + "//post2";
}
module.exports = loader;
loaders/pre-loader1.js
function loader(source) {
console.log("pre1");
return source + "//pre1";
}
module.exports = loader;
loaders/pre-loader2.js
function loader(source) {
console.log("pre2");
return source + "//pre2";
}
module.exports = loader;
runner.js
let path = require("path");
let fs = require("fs");
let { runLoaders } = require("loader-runner");
let loaderDir = path.resolve(__dirname, "loaders");
let request = "inline-loader1!inline-loader2!./index.js";
//最前面的前缀去掉,多个!合并成一个
let inlineLoaders = request
.replace(/^-?!+/, "")
.replace(/!!+/g, "!")
.split("!");
let resource = inlineLoaders.pop(); // 获取资源的路径 resource = index.js
//传进去一个loader的相对路径,返回一个绝对路径
let resolveLoader = (loader) => path.resolve(loaderDir, loader);
//从相对路径变成绝对路径
inlineLoaders = inlineLoaders.map(resolveLoader);//[inline-loader1,inline-loader2]
// 模拟webpack中rules的配置
let rules = [
{
enforce: "pre", //指定loader的类型 前置
test: /\.js?$/,
use: ["pre-loader1", "pre-loader2"],
},
{
test: /\.js?$/,
use: ["normal-loader1", "normal-loader2"],
},
{
enforce: "post", //指定loader的类型 后置
test: /\.js?$/,
use: ["post-loader1", "post-loader2"],
},
];
let preLoaders = [];
let postLoaders = [];
let normalLoaders = [];
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
if (rule.test.test(resource)) {
if (rule.enforce == "pre") {
preLoaders.push(...rule.use);
} else if (rule.enforce == "post") {
postLoaders.push(...rule.use);
} else {
normalLoaders.push(...rule.use);
}
}
}
preLoaders = preLoaders.map(resolveLoader);
postLoaders = postLoaders.map(resolveLoader);
normalLoaders = normalLoaders.map(resolveLoader);
let loaders = [];
//noPrePostAutoLoaders 忽略所有的 preLoader / normalLoader / postLoader
if (request.startsWith("!!")) {
loaders = inlineLoaders; //只保留inline
//noPreAutoLoaders 是否忽略 preLoader 以及 normalLoader
} else if (request.startsWith("-!")) {
loaders = [...postLoaders, ...inlineLoaders]; //只保留post和inline
//是否忽略 normalLoader
} else if (request.startsWith("!")) {
loaders = [...postLoaders, ...inlineLoaders, ...preLoaders]; //保留post inline pre
} else {
loaders = [...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders];
}
//console.log(loaders);
runLoaders(
{
//包含查询字符串的资源绝对路径
resource: path.join(__dirname, "./src/index.js"),
//String[]:loader的绝对路径字符串数组(可以包括查询字符串)
//{loader, options}[]: 使用参数对象的loaders的绝对路径
loaders,
//额外的上下文信息
context: { minimize: true },
//一个用来读取资源的函数 必须拥有签名 function(path, function(err, buffer))
readResource: fs.readFile.bind(fs),
},
/**
*
* @param {*} err 错误对象
* @param {*} result
* result Buffer或者String
* resourceBuffer Buffer
* cacheable 结果是否可缓存还是需要重新执行
* fileDependencies 结果依赖的文件列表字符串数组
* contextDependencies 结果依赖的字符串列表字符串数组
*/
function (err, result) {
console.log(err);
console.log(result);
}
);
在vscode右键run code运行runner.js文件,输出结果如下
可以看到loader的执行顺序是pre -> normal -> inline -> post
所有普通 loader 可以通过在请求中加上 ! 前缀来忽略(覆盖)。
所有普通和前置 loader 可以通过在请求中加上 -! 前缀来忽略(覆盖)。
所有普通,后置和前置 loader 可以通过在请求中加上 !! 前缀来忽略(覆盖)。
不应使用内联 loader 和 ! 前缀,因为它是非标准的。它们可能会被 loader 生成代码使用。
| 符号 | 变量 | 含义 |
|---|---|---|
| -! | noPreAutoLoaders | 不要前置和普通loader |
| ! | noAutoLoaders | 不要普通 loader |
| !! | noPrePostAutoLoaders | 不要前后置和普通 loader,只要内联 loader |
我们可以通过修改上面runner.js中的request来测试。
修改runner.js中的request如下
let request = "!!inline-loader1!inline-loader2!./index.js";
运行runer.js,输出如下,可以看出只有内联 loader

修改runner.js中的request如下
let request = "-!inline-loader1!inline-loader2!./index.js";
运行runer.js,输出如下,可以看出只有行内和内联loader

修改runner.js中的request如下
let request = "!inline-loader1!inline-loader2!./index.js";
运行runer.js,输出如下,可以看出只有前置、行内和内联loader

以上并不是loader执行的整个过程,loader的完整的顺序有两个阶段:pitching和normal阶段。上面介绍的只是normal阶段的执行顺序
Rule.enforce官网解释 (opens new window)
- Pitching 阶段: loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。更多详细信息,请查看 Pitching Loader。
- Normal 阶段: loader 上的 常规方法,按照 前置(pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。模块源码的转换, 发生在这个阶段。
loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。
如果某个 loader 在 pitch 方法中给出一个结果,那么这个过程会回过身来,并跳过剩下的 loader。
大神的解释如下:
It's like the two phases of event bubbling...
a!b!c!resource
pitch a
pitch b
pitch c
read file resource (adds resource to dependencies)
run c
run b
run a
When a loader return something in the pitch phase the process continues with the normal phase of the next loader... Example:
pitch a
pitch b (returns something)
run a
- 比如 a!b!c!module, 正常调用顺序应该是 c、b、a,但是真正调用顺序是 a(pitch)、b(pitch)、c(pitch)、c、b、a,如果其中任何一个 pitching loader 返回了值就相当于在它以及它右边的 loader 已经执行完毕
- 比如如果 b 返回了字符串"result b", 接下来只有 a 会被系统执行,且 a 的 loader 收到的参数是 result b
- loader 根据返回值可以分为两种,一种是返回 js 代码(一个 module 的代码,含有类似 module.export 语句)的 loader,还有不能作为最左边 loader 的其他 loader
- 有时候我们想把两个第一种 loader chain 起来,比如 style-loader!css-loader! 问题是 css-loader 的返回值是一串 js 代码,如果按正常方式写 style-loader 的参数就是一串代码字符串
- 为了解决这种问题,我们需要在 style-loader 里执行 require(css-loader!resources)
修改loaders/post-loader1.js如下
function loader(source) {
console.log("post1");
return source + "//post1";
}
loader.pitch = function(){
console.log('post1 pitch')
}
module.exports = loader;
修改loaders/post-loader2.js如下
function loader(source) {
console.log("post2");
return source + "//post2";
}
loader.pitch = function(){
console.log('post2 pitch')
}
module.exports = loader;
修改loaders/normal-loader1.js如下
function loader(source) {
console.log("normal1");
return source + "//normal1";
}
loader.pitch = function(){
console.log('normal1 pitch')
return 'normal1pitch';
}
module.exports = loader;
修改loaders/normal-loader2.js如下
function loader(source) {
console.log("normal2");
return source + "//normal2";
}
loader.pitch = function(){
console.log('normal2 pitch')
return 'normal-loader2-pitch';
}
module.exports = loader;
运行runner.js,输出结果如下

可以看出先执行的post1的pitch,再执行的post2的pitch,当执行到normal1的pitch后,由于normal1的pitch有返回值,所以剩下剩下pitch和loader不再执行直接返回,执行inline loader和 post loader。
