前言
作为切图仔,我们对webpack肯定不陌生。而其中有一个名为loader的配置,大家更是熟悉,基本没有哪一个使用webpack的前端项目能离开这个配置项,因为webpack并不认识除了js和json之外的文件,所以除了这些webpack认识的文件之外,就需要配置loader去将不认识的文件翻译成认识的,说到底,loader就是个翻译官。
虽然我们非常熟悉loader如何配置,但我们很可能存在一个不完善的概念,那就是无论是我们刚接触loader或者是背八股文时被灌输的一个概念,那就是,loader是按照从右到左的顺序执行的
,所以loader的配置也是遵守着从右到左。这句话是没问题
的,但它不完整
。
默默无闻的pitch
其实在loader中还有这么一个角色,名为pitch
,大家可能没听说过它,但它一直默默无闻的为大家工作着,最典型的就是我们style-loader
了。
一般来说loader就是一个函数,但js中,万物皆对象,函数也就自然是对象了,既然是对象,它能有用一个名为pitch的属性,没毛病吧。而这个pitch其实也是一个函数。这种拥有pitch的loader表现形式如下。
js
function loader1() {};
loader1.pitch = () => {};
module.exports = loader1;
完整的loader执行顺序
完整的执行顺序其实是pitch从左到右,然后loader从右到左。
此时,我们假设配置了三个loader且他们都有用各自的pitch,他们分别为loader1(pitch1),loader2(pitch2),loader3(pitch3),那他们的执行顺序如下图。
这里要注意一下这个大标题,没有任何一个pitch有返回值
,此时才是上图的这个执行顺序。
pitch是极为霸道的存在,如果某一个pitch有返回值,它将直接跳过后面的pitch和loader的执行,直接跳到上一个loader的执行,并且这个pitch的返回值会传给上一个loader这里。
上面这句话或许有点难以理解,我们来看图。
结合图来看,pitch2有返回值,那么它直接跳过了后面的pitch和loader(pitch3,loader3,loader2)的执行,然后去执行上一个loader(loader1)。这里再次强调一下,是跳过,意思就是不会再执行那些跳过的东西了。是不是体会到pitch的霸道了?
验证
想要验证上面所说的其实很简单,我们创建一个项目。
webpack.config中针对js文件的解析,采用我们自己写的loader。
js
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules|loaders/,
use: [
path.resolve(__dirname, 'loaders/loader1.js'),
path.resolve(__dirname, 'loaders/loader2.js'),
path.resolve(__dirname, 'loaders/loader3.js'),
],
}
],
},
然后我们分别写下这三个loader
js
loaders/loader1.js
function loader(source) {
console.log('loader1');
return source;
}
loader.pitch = () => {
console.log('pitch1');
};
module.exports = loader;
loaders/loader2.js
function loader(source) {
console.log('loader2');
return source;
}
loader.pitch = () => {
console.log('pitch2');
};
module.exports = loader;
loaders/loader3.js
function loader(source) {
console.log('loader3');
return source;
}
loader.pitch = () => {
console.log('pitch3');
};
module.exports = loader;
最后在package.json中配置一个build命令。
js
"scripts": {
"build": "webpack"
},
执行build命令。
至于pitch有返回值的情况,就让各位自行写demo测试了。
style-loader
style-loader这个loader想必大家都配置过,而且它的功能大家也非常清楚,就是加一个style标签而已,但它是使用pitch的最经典例子了。我们这一小节就来讲讲它如何使用pitch的。
style-loader完美的利用了pitch的霸道性,它不希望自己之后的pitch执行,所以它在自己的pitch中加了返回值,跳过了后面的pitch和loader。但这引发另外一个问题,那就是后面的loader是需要执行的,因为style-loader自身并不认识less、sass等文件,他需要相应的loader进行解析,因此,style-loader将后面的loader都采用了另一种方式
去调用和执行。
这种所谓的另外一种方式是什么呢?其实大家都很熟悉,那就是require
。我们来看下style-loader的pitch的源码(简化后的)。
js
// style-loader
// remainingRequest是剩余请求,也就是后面未执行的loaders加上一个源文件
loader.pitch = function(remainingRequest) {
// 1 获取剩下的请求 remainingRequest是由!链接的路径(laoder1!loader2!file)其实就是内联loader的形式
// 2 分割出各个请求的路径,前面是loader,最后一个是文件路径
// 3 把路径从绝对路径编程相对于根目录的相对路径,其实就是模块Id // 路径前面要加上!!,只使用行内loader,不使用rule里面配置的loader,否则会死循环
const request = '!!' + (remainingRequest.split('!')
.map( requestAbsPath => this.utils.contextify(this.context, requestAbsPath) )
.join('!'));
let script = `
let styleCSS = require(${JSON.stringify(request)});
let style = document.createElement('style');
style.innerHTML = styleCSS; document.head.appendChild(style);
`;
return script;
};
上面这段代码,大家可能对第6行会有所疑惑,这里我们将引出另外一个概念,那就是loader的分类和loader的特殊配置。
loader的分类
- post:后置
- inline:内联
- normal:正常
- pre:前置
这种分类与loader本身是没有关系的,只是跟使用loader时的配置有关。因为在loader配置中,它可能是由多个文件合并而来的,为了保证执行的时候按照特定的执行顺序来执行,所以给loader进行了分类。
他们的加载顺序是:pre => inline => normal => post
loader的特殊配置
loader还有特殊配置,在资源模块前添加,内联的方式。
- -!:名为noPreAutoLoaders,指不要pre和normal
- !:名为noAutoLoaders,指不要normal
- !!:名为noPrePostAutoLoaders,指只要inline
上面这些分类和配置有什么用呢?
这里我们就不细说都分别有什么用,我们把目光放回到style-loader代码的第6行。
js
const request = '!!' + (remainingRequest.split('!')
.map( requestAbsPath => this.utils.contextify(this.context, requestAbsPath) )
.join('!'));
'!!'双感叹,noPrePostAutoLoaders,只要内联loader。为什么要这么写?
答案其实很简单,避免死循环。因为当我们加载到指定类型文件时(如less),webpack就会根据loader配置中的正则去匹配,而我们这个代码中的request的最后是有一个file的,也就是文件,webpack再加载这个文件的时候又会匹配到webpack.config中针对该文件类型配置的loader,从而触发死循环。
下图就是假设不是只要内联loader的情况。
结尾
好了,到这里我们就将loader完整的执行顺序讲了一遍。
本文通过loader的完整执行顺序引出了pitch相关知识,然后再扩展了loader的分类以及特殊配置两个小概念,之后是完整讲述了style-loader的执行并且通过style-loader的执行更全面得认识pitch。
那么本文到此结束,都看到这里了,各位观众老爷觉得本文有帮助的话,点个赞吧🌹