你了解webpack中配置的loader的执行顺序吗?
为什么要关注loader的执行顺序?
最近在工作中需要写几个webpack的loader,但是发现好像自己并不清楚每次在webpack中配置的loader执行的顺序是如何的,可能只有我不太清楚吧。。😓 所以想写一个小demo把玩把玩~
```js { test: /.scss$/, use: [ 'style-loader',
// MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
sourceMap: true
}
},
'sass-loader'
]
}
```
如果你已经知道上面这几个loader都是做什么的话,那你应该已经“大概”知道loader的执行顺序了,如果不知道的话,还请客官继续往下看看~
loader在同一个rule中的执行顺序
这里因为我想要知道在webpack中配置loader的执行顺序,所以我写了一个简单的demo用webpack进行打包,加入了几个简单的js loader:
- 我们把重点放到webpack配置中module的rule中:
js
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: require.resolve('./src/loaders/loader1.js'),
},
{
loader: require.resolve('./src/loaders/loader2.js'),
},
{
loader: require.resolve('./src/loaders/loader3.js'),
}
]
},
]
}
- demo中的入口文件只是简单定义了几个变量,并且log出index的文件名
```js
// webpack打包的入口entry文件: index.js
const index = 'index.js';
const loader1 = 'loader1.js';
const loader2 = 'loader2.js';
const loader3 = 'loader3.js';
console.log(index)
- demo中再加入几个简单的loader,loader1,loader2,loader3都是一个简单loader,他们三个的内容非常简单,只是简单的加入`console.log('文件名字对应的变量名')`,这样方便测试最终打出来的bundle.js中所包含的内容
js
// loader1.js中输出的是loader1, loader2.js中输出的是loader2,loader3.js输出loader3
module.exports = function(source, options) {
const str = '\n console.log(loader1);'
console.log('executed in loader1')
return source + str
};
```
- 那么最终打包出来的结果是什么样子的呢?
从下图能够看出来,入口文件index.js分别经过了三个loader处理,从后向前执行.
- 即先经过loader3加入了
console.log(loader3)
. - 再loader2处理就加入了
console.log(loader2)
- 最后经过loader1处理加入了
console.log(loader1)
所以执行顺序就是loader3 -> loader2 -> loader1
loader在多个rule中的执行顺序
多个rule中每个loader都不同
接下来再做个实验,如果我配置三个相同的rule,里面的loader的执行顺序又是啥样的呢? ```js module: { rules: [ { test: /.js$/, use: [ { loader: require.resolve('./src/loaders/loader1.js'), } ] }, { test: /.js$/, use: [ { loader: require.resolve('./src/loaders/loader2.js'), } ] }, { test: /.js$/, use: [ { loader: require.resolve('./src/loaders/loader3.js'), } ] },
]
}
``
结果如下:
![image.png](http://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/76ff9415b2394d2dabd80496b2219b79~tplv-k3u1fbpfcp-watermark.image?)
emm。。。结果和上面的
loader在同一个rule中的执行顺序`一致,和我想的一样。打包过程的终端中输出的内容是:
执行顺序也是loader3 -> loader2 -> loader1
多个rules中有相同的loader
```js module: { rules: [ { test: /.js$/, use: [ { loader: require.resolve('./src/loaders/loader1.js'), }, { loader: require.resolve('./src/loaders/loader2.js'), } ] }, { test: /.js$/, use: [ { loader: require.resolve('./src/loaders/loader1.js'), }, { loader: require.resolve('./src/loaders/loader2.js'), }, { loader: require.resolve('./src/loaders/loader3.js'), } ] },
]
},
```
打包过程中输出的终端内容为:
果然就是倒着执行嘛,从右向左执行,执行顺序是3 -> 2 -> 1 -> 2 -> 1,好像loaders中连去重都不会,也就是说你的loader配置了几次就会被执行几次,此时我的问题就来了,那么有没有可能我在执行3 -> 2 -> 1的时候在某种情况下并不想继续执行了,也就是说给loader的执行顺序中加入逻辑?带着疑问我点开了
node_modules/webpack/lib/NormalModule.js
中找到了node_modules/loader-runner.js
文件,里面有一个runLoaders的方法。。。至此开启了一趟奇妙的旅程。。
谢特! BRO
-
原来以为loader的执行顺序无非就是一个数组的pop,push之类的,但当我看到了这里的代码的时候发现远比我想象中的复杂。从下面的代码片段中,前面的过程看似还比较容易理解,都是向locaderContext上面注入一些变量,比如remainingRequest: 剩余的loaders。previousRequest: 之前执行过的loaders等等,那么后面的这个iteratePitchingLoaders是什么鬼?pitching又是什么? ```js exports.runLoaders = function runLoaders(options, callback) { ... var loaders = options.loaders || []; var loaderContext = options.context || {}; Object.defineProperty(loaderContext, "resource", { ... }); Object.defineProperty(loaderContext, "request", { ... }); Object.defineProperty(loaderContext, "remainingRequest", { ... }); Object.defineProperty(loaderContext, "currentRequest", { ... }); Object.defineProperty(loaderContext, "previousRequest", { ... }); Object.defineProperty(loaderContext, "query", { ... }); Object.defineProperty(loaderContext, "data", { ... });
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { ... }); }; ``` 此处查询了一下webpack文档的内容,原来每个loader的执行顺序其实由两部分组成: 1. Pitching 过程,loaders的pitching过程从前到后(loader1 -> 2 -> 3) 2. Normal 过程, loaders的normal过程从后到前(loader3 -> 2 -> 1)
此时我们稍微修改一下loader中的内容:loader中再加入pitch方法:
js
// loader1.js中输出的是loader1, loader2.js中输出的是loader2,loader3.js输出loader3
module.exports = function(source, options) {
const str = '\n console.log(loader1);'
console.log('executed in loader1')
return source + str
};
// 下面的内容是向loader中需要添加的
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
console.log('pitch in loader1')
};
输出瞅一眼:
看起来和文档上写的一样,整个过程有点像eventListener的冒泡和捕获过程。
iteratePitchingLoaders
这里我们再看一下iteratePitchingLoaders的内容是什么(已经简化) ```js function iteratePitchingLoaders(options, loaderContext, callback) { if(loaderContext.loaderIndex >= loaderContext.loaders.length) { //递归loaders,当目前的index大于loaders的数量的时候,即所有loader的pitching都执行完毕 processResource() //执行loader的normal阶段 } if(currentLoaderObject.pitchExecuted) { // 如果当前loader的pitching执行过了 loaderContext.loaderIndex++; // index加一 return iteratePitchingLoaders(options, loaderContext, callback); // 递归调用下一个loader的pitching函数 }
loadLoader(currentLoaderObject, function(err) {
var fn = currentLoaderObject.pitch; // 拿到当前loader的pitching函数
currentLoaderObject.pitchExecuted = true; //pitched标志位置true,用作下次递归
if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); // 如果没有pitching函数,就递归下一个loader的pitching
runSyncOrAsync(fn, function(err) { // 运行pitching(即fn)方法 将结果传入callback
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
// 这里的args是pitching执行之后返回的内容
if(args.length > 0) {
//如果当前loader的pitching方法有返回内容,则执行前一个函数的normal阶段
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback) ;
} else {
// 如果当前的pitching函数没有返回值,递归下一个laoder的pitching
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
});
} ``` 就是先顺序执行所有loaders的pitching,再倒序执行normal!
影响loader执行顺序因素之一:pitching方法的返回内容
当看到runSyncOrAsync中的内容时我们发现,当一个loader的pitching函数有返回值的时候,就会跳过之后的步骤,直接来到前一个loader的normal阶段,如下图:
现在稍微更改一下我们的loader2,在它的pitching中加入返回值: ```js module.exports = function(source, options) { const str = ' \n console.log(loader2);' console.log('executed in loader2')
return source + str
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) { console.log('pitch in loader2') return '123'; }; ```
果然是这样!那么此时利用这个pitching的特性,是不是就可以给loader的执行顺序中加入逻辑?目前来看,我只知道pitching返回一个值是可以直接跳到上一个loader的normal阶段,那么如果有更复杂的逻辑该怎么办呢?
影响loader执行顺序因素之二:Rule.enforce的配置
在查看文档
的时候,还看到一个配置:rule.enforce: pre/post/normal
这个配置也会影响loader的执行顺序如下:
我们在我们的demo中实验一下:(在上一部分我们在loader2的pitching中加入了返回值,现在要去掉,以免影响我们测试enforce属性对顺序的影响)
js
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: [
{
loader: require.resolve('./src/loaders/loader1.js'),
},
]
},
{
test: /\.js$/,
use: [
{
loader: require.resolve('./src/loaders/loader2.js'),
}
]
},
{
test: /\.js$/,
enforce: 'post',
use: [
{
loader: require.resolve('./src/loaders/loader3.js'),
}
]
},
]
影响loader执行顺序因素之三:inline-loader
inline-loader是除开pre, normal, post三种loader之外的另外一种loader,这种loader文档中并不建议我们自己手动加入,而是应该由其他的loader自动生成,当inline-loader加入全家桶之后loader的执行顺序如下:
它的使用方式是这样的:
requre("!!path-to-loader1!path-to-loader2!path-to-loader3!./sourceFile.js")
抛开'!!, !, -!'等标识来看,从右向左来看就是让sourceFile.js分别通过loader3,loader2,loader1三个loader来进行处理。
- !表示所有的normal loader全部不执行(执行pre,post和inline loader)
- -!表示所有的normal loader和pre loader都不执行(执行post和inline loader)
- !! 表示所有的normal pre 和 post loader全部不执行(只执行inline loader)
不太懂?没关系,我们在demo中验证一下: 1. 首先加入loader4,其内容和loader3一致 ```js module.exports = function(source, options) { const str = ' \n console.log("I am in the inline loader");' console.log('executed in loader4') return source + str
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
console.log('pitch in loader4')
};
2. webpack配置的rule中的配置修改如下:
js
rules: [
{
enforce: 'post', //让loader1的类型变为post loader
test: /.js$/,
use: [
{
loader: require.resolve('./src/loaders/loader1.js'),
},
]
},
{
test: /.js$/,
use: [
{
loader: require.resolve('./src/loaders/loader2.js'),
}
]
},
{
enforce: 'pre', // loader3的类型变为pre loader
test: /.js$/,
use: [
{
loader: require.resolve('./src/loaders/loader3.js'),
}
]
},
]
```
在之前demo的基础上我们将loader1变为了post-loader,loader3变为了pre-loader,目前执行的顺序此时还是loader3 -> loader2 -> loader1
-
loader2中的pitching中也要做出修改: ```js module.exports = function(source, options) { const str = ' \n console.log(loader2);' console.log('executed in loader2')
return source + str };
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
console.log('pitch in loader2')
return require('!!./src/loaders/loader4.js!./index.js')
// return一个inline-loader的调用
};
```
此时我们的demo中有了post-loader(loader1),pre loader(loader3),normal loader(loader2)和inline loader(loader2中return的loader4)。只有loader2的pitching有返回值,开始编译!结果如下:
loader2中返回require('!!./src/loaders/loader4.js!./index.js')
!!
意思是只执行inline-loader,那么我们的顺序如下:
1. 执行loader1的pitching (webpack没有感知到又loader4这个inline-loader)pitching loader 1
2. 执行loader2中的pitching,此时loader2中返回了inline-loader(感知到了loader4)pitching loader 2
3. 因为loader2的pitching返回内容了,回马枪执行了loader1的normal(第一轮结束) executed loader1
4. 因为loader4加入全家桶,!!
最后只执行inline-loader类型的loader4的pitching和normal
5. pitching loader4
6. executed loader4
loader2中返回require('-!./src/loaders/loader4.js!./index.js')
-!
意思是只执行inline-loader和post-loader,那么我们的顺序如下:
1. 执行loader1的pitching (webpack还没有感知到有loader4这个inline-loader)pitching loader 1
2. 执行loader2中的pitching,此时loader2中返回了inline-loader(感知到了loader4)pitching loader 2
3. 因为loader2的pitching返回内容了,回马枪执行了loader1的normal (第一轮结束)executed loader1
4. 因为loader4加入全家桶,-!
只会执行post(loader1)和inline(loader4),
5. 按照四种loader的顺序先执行post-loader的pitching pitching loader1
6. 再执行inline-loader的pitching pitching loader4
7. 接着inline-loader的normal executed loader4
8. 接着post-loader的normal (第二轮结束)executed loader1
loader3中返回require('!./src/loaders/loader4.js!./index.js')
!
意思是只执行inline-loader和post-loader和pre-loader,那么我们的顺序如下:
- 执行loader1的pitching (webpack没有感知到又loader4这个inline-loader)pitching loader 1
- 执行loader2中的pitching,此时loader2中返回了inline-loader(感知到了loader4)pitching loader 2
- 因为loader2的pitching返回内容了,回马枪执行了loader1的normal (第一轮结束)executed loader1
- 因为loader4加入全家桶,
!
只会不执行normal(loader2) - 按照四种loader的顺序先执行post-loader的pitching pitching loader1
- 再执行inline-loader的pitching pitching loader4
- 再执行pre-loader的pitching pitching loader3
- 再执行pre-loader的normal executed loader3
- 接着inline-loader的normal executed loader4
- 接着post-loader的normal (第二轮结束)executed loader1
style-loader,css-loader,sass-loader的真实执行顺序:
如果你打开style-loader的文件,你会看到大概下面的内容: ```js module.exports = function () {};
module.exports.pitch = function (request) { // request是remianing的loader
return [
var content = require(" + ${loaderUtils.stringifyRequest(this, "!!" + request) + ");}
,
var update = require(" + ${loaderUtils.stringifyRequest(this, "!" + path.join(__dirname, "lib", "addStyles.js")) + ")(content, options);}
,
"",
"module.exports = content.locals;",
],
}
```
style-loader中根本没有normal过程,而是pitching过程,并且pitching返回了inline-loader!!
style-loader -> css-loader -> sass-loader真实的执行顺序其实是: 1. 先经过style-loader的pitching,此时pitching返回值有内容,简化为require('!!css-loader/index.js!sass-loader/dist/cjs.js!./index.sass'),第一轮直接结束。 2. 因为style-loader的pitching返回了内容,所以剩下的loader阶段都不执行,转而执行inline-loader的内容(inline-loader中又包含了两个loader,是从后向前执行的,即现sass-loader再css-loader) 3. 在inline-loader中,sass-loader对index.sass处理,将sass内容处理成css。 4. css-loader对 “3” 中执行之后内容进行处理,css-loader将css转换成js字符串。 5. 此时回到style-loader中的pitching,“4”之后的结果将被style-loader剩下的逻辑处理'addStyles',即加到style标签上再append到dom上。
总结:
loader的真实执行顺序和他们在rule中配置的顺序、类型(pre,normal,post,inline)、以及loader中在pitching中返回的内容都有关!
reference: http://zhuanlan.zhihu.com/p/360421184