Webpack:Loader执行策略,如何自定义Loader

141 阅读6分钟

Loader执行策略

我们在使用webpack对代码进行处理时,难免会用到loader对文件进行处理,多个loader对一个文件进行处理,难免会涉及到哪些loader先执行的问题,下面让我们来了解一下。

首先,我们所写的loader是从下到上执行的,假设我们有如下配置

module: {
  rules: [
    {
      test: /\.js$/,  // 匹配以 .js 结尾的文件
      use: [
        "loader01",   // 第一个加载器
        "loader02",   // 第二个加载器
        "loader03",   // 第三个加载器
      ]
    }
  ]
}

这三个loader的执行顺序是 03 > 02 > 01 ,为什么执行顺序是从下向上进行处理呢?

每个loader还有一个属性pitch,是一个函数(loader本身也是一个函数) ,我们称这个属性为pitchloader,而我们的loader称为normalloader,runloader函数在遍历执行loader时(runloader是源码中的函数,想要了解webpack源码,可以看我的另一篇文章webpack源码解析),它首先遍历pitchloader,并将index++,在遍历normalloader,并将index--,这样便形成了先正着调用pitchloader,在反向调用normalloader,具体流程如下

其中pitchloader并不接受源文件,但偶尔需要它处理源文件,我们下文中会讲到

而normalloader就是主要的处理源文件的函数了,他们在处理完源文件后会交给下一个loader再次处理,我们现在就是要发现,他们的先后执行顺序。

上文说了,执行顺序是正序执行pitchloader,逆序执行normal,那么有什么方法可以配置他们的顺序吗?当然有

rules: [
  {
    test: /\.js$/,  // 匹配以 .js 结尾的文件
    use: [
      {
        loader: "loader01",
        enforce: "pre"  // 在其他加载器之前执行
      },
      {
        loader: "loader02",
        enforce: "post" // 在其他加载器之后执行
      },
      {
        loader: "loader03",
      },
    ]
  }
]

如图所示,当我们给每个loader分别配置enforce属性,顺序就被打乱了,首先pre会最先执行,不配置的默认未normal,配置post的最后执行,与之相反的是pitchloader,他与normalloader执行顺序相反。

除了上面三种loader,还有一种inlineloader,他的执行顺序是在post之前,为什么要单独拿出来说呢,因为它有一些不一样。如下所示:

require('!!./src/loaders/loader4.js!./index.js')

代码中路径,后面是要解析的文件,解析文件前面是一个感叹号,这个感叹号的作用是阻隔loader的路径和被解析文件的,再向前有两个感叹号,还可以设置成!以及-!,具体含义如下

  • !表示所有的normal loader全部不执行(执行pre,post和inline loader)
  • -!表示所有的normal loader和pre loader都不执行(执行post和inline loader)
  • !! 表示所有的normal pre 和 post loader全部不执行(只执行inline loader)

其中,我们写在require中的loader(这个loader4是不再rule中的)便被成为inlineloader

这段代码通常在pitchloader中调用,在执行这段代码之后,执行完这次的全部loader(如果在pitch loader中调用,那么本次loader将不会全部执行,因为pitchloader具有熔断效果,下面我就会将),还会出现第二论loader调用,针对这个路径下的文件,并且根据!决定调用那些loader,并且按照上文中说的顺序来执行。

loader的执行顺序,还与pitchloader有关,不仅仅是他的优先执行,还有它具有的熔断效果。

什么是熔断效果,让我们看下面这个图

一旦pitch返回了不为undifend的值,loader就不会继续执行,而是会跳到上一个loader的normalloader中执行代码,如果此次为第一个pitchloader的话,那么本次loader的处理直接结束,不过通常在pitch有返回值时,pitchloader都是会调用inlineloader的,所跳出第一轮loader是为了执行某些功能,并不是不使用loader解析代码了,后文我会给出例子。

pitchloader不同于normal接收文件,处理文件,它具有三个参数,分别为

  • remainingRequest:在本次loader之后的loader(不包括本次,并且为路径)
  • previousRequest:在本次loader之前的loader(不包括本次,并且为路径)
  • data:数据,pitchloader中定义,可以在对应的normalloader中获取

那么pitchloader到底有什么作用呢,其实我也不太清楚,但我给大家讲一个大家熟知的例子,大家自己理解吧,那就是cssloder和styleloader

这个两个loader大家都很熟悉,我们经常使用,并且我们要把styleloader写在上面,cssloader写在下面,这样让cssloder先解析,将路径内文件进行解析,在由styleloader将解析结果插入打包结果中,完成对css的打包。

这些是大家都熟知的,那么我来说一些大家可能不知道的,那就是styleloader的功能其实是在pitchloader中实现的,经过上面的学习,我们大家知道了,pitchloader是要比normalloader先执行的,那styleloader不就比cssloder先执行了吗?没错,styleloader确实是最先发挥作用的,但是它仍然需要cssloader先进行解析,得到他的结果,那他是怎么做到的呢?

import content, * as namedExport from "!!!../../../node_modules/css-loader/dist/cjs.js!./a.css";

这是styleloader的pitch中的代码,styleloader的pitch通过inlineloader的方式调用cssloder处理css文件,并且拿到最终返回值在进行处理,最终返回返回值,终止loader的继续解析,这样即使styleloader的pitch在cssloader的normal前执行,也能拿到cssloader处理代码的返回值,但为什么要这么做呢?这不是多此一举吗?

问题就出在cssloader处理完代码,返回给下个loader函数的参数是一个模块,函数接收模块为参数,闻所未闻,根本不知道怎么拿到里面的导出值,所以styleloader不得已在pitch中通过上图方法来调用cssloader来处理代码,这样styleloader可以通过import再将cssloader返回的模块内部的导出值引入。

讲完这个例子,大家就可以自行理解pitchloader的作用了。

自定义Loader

Webpack中loader本质就是函数,其中前一个loader处理完代码后,交给后一个代码继续处理,最终经过多个loader的处理后,源代码变成最终代码。

我们可以创建一个customLoader.js文件,并在其中定义并导出如下函数:

module.exports = function(content) {
  /* 对传入模块进行一些处理 */
  // 返回处理后的 content,返回后的结果会传给下一个Loader
  return content;
}

然后在webpack的resolveLoader配置项中配置文件路径:

resolveLoader: {
  modules: ["./customLoader"]
},

文件名将会作为这个Loader的名字,我们则可以将customLoader配置到rules中:

module: {
  rules: [
    {
      test: /\.js$/,
      use: [
        {
          loader: "customLoader",
          options: {
            plugins: [],
            presets: []
          }
        }
      ]
    }
  ]
}

这样一个简单的loader就创建好了,在配置rules的loader时,还会配置options配置,我们可以通过再customLoader的函数中通过this.getOptions来获取配置项中的options配置,这样便可以根据配置项,动态改变loader功能。

Loader默认为同步Loader,我们可以通过一些方式将其变为异步Loader

1. 返回Promise对象:

module.exports = function(content) {
  return Promise((resolve) => {
    setTimeout(() => {
      // resolve的值相当于同步loader的返回值
      resolve('content')
    }, 3000)
  })
}

2. 调用this.async():

通过调用this.async(),可以讲当前Loader变为异步函数,并且会返回一个回调函数,类似于Promise对象的resolve函数,用于返回结果。

module.exports = function(content) {
  const callback = this.async();

  setTimeout(() => {
    callback(null, content);  // 异步操作完成后,调用 callback 继续执行
  }, 1000);
};

3. 自定义pitchloader

我们可以通过在定义普通Loader文件中,在导出一个module.exports.pitch的函数,来实现给Loader设置pitch属性,这个属性的函数就是pitchloader,代码如下:

module.exports = function(content) {
  return content;
}

module.exports.pitch = function() {
  // pitch 函数的内容
}