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。