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 函数的内容
}