loader从右到左(或从下到上)的执行。 在实际执行loader之前,会先 从左到右 调用loader上的pitch方法。如果pitch方法给出了一个结果, 那么pitch对应的那个normalLoader就不执行了。
- 源码解析
//loader-runner/lib/LoaderRunner
exports.runLoaders = function runLoaders(options, callback) {
// read options
var resource = options.resource || "";
var loaders = options.loaders || [];
var loaderContext = options.context || {};
...
// 调用iteratePitchingLoaders
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
if(err) {
return callback(err, {
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies,
missingDependencies: missingDependencies
});
}
callback(null, {
result: result,
resourceBuffer: processOptions.resourceBuffer,
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies,
missingDependencies: missingDependencies
});
});
从左到右遍历loaders,执行loader下的pitch方法
function iteratePitchingLoaders(options, loaderContext, callback) {
// abort after last loader
// PitchingLoaders遍历完后,调用processResource开始从右至左遍历NormalLoaders
if(loaderContext.loaderIndex >= loaderContext.loaders.length)
return processResource(options, loaderContext, callback);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// iterate
// 判断当前loader的pitch方法是否已经执行,已经执行,则执行下一个
if(currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
// load loader module
loadLoader(currentLoaderObject, function(err) {
if(err) {
loaderContext.cacheable(false);
return callback(err);
}
// 获取loader上的pitch方法
var fn = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
// 调用runSyncOrAsync同步或异步执行loader上的pitch方法
runSyncOrAsync(
fn,
loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
// Determine whether to continue the pitching process based on
// argument values (as opposed to argument presence) in order
// to support synchronous and asynchronous usages.
var hasArg = args.some(function(value) {
return value !== undefined;
});
if(hasArg) {
loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
});
}
function processResource(options, loaderContext, callback) {
// set loader index to last loader
// loaderContext.loaders.length 一个文件对应的所有loader的长度
// 先获取loaders数组中的最后一个
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
var resourcePath = loaderContext.resourcePath;
if(resourcePath) {
options.processResource(loaderContext, resourcePath, function(err, buffer) {
if(err) return callback(err);
options.resourceBuffer = buffer;
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
} else {
iterateNormalLoaders(options, loaderContext, [null], callback);
}
}
function iterateNormalLoaders(options, loaderContext, args, callback) {
if(loaderContext.loaderIndex < 0)
return callback(null, args);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// iterate
// 判断当前loader是否已经执行
if(currentLoaderObject.normalExecuted) {
// loaderIndex-- 所以是从数组的最后一个元素往前遍历
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}
var fn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
if(!fn) {
return iterateNormalLoaders(options, loaderContext, args, callback);
}
convertArgs(args, currentLoaderObject.raw);
// 调用runSyncOrAsync执行loader
runSyncOrAsync(fn, loaderContext, args, function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
- 测试用例
// module.rules的配置:
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader','sass-loader', 'postcss-loader'],
},
// 运行结果如下:
0
D:\code\web_learn\webpack\demo\index.sass
D:\code\web_learn\webpack\demo\node_modules\style-loader\dist\cjs.js
2
D:\code\web_learn\webpack\demo\index.sass
D:\code\web_learn\webpack\demo\node_modules\postcss-loader\dist\cjs.js
1
D:\code\web_learn\webpack\demo\index.sass
D:\code\web_learn\webpack\demo\node_modules\sass-loader\dist\cjs.js
0
D:\code\web_learn\webpack\demo\index.sass
D:\code\web_learn\webpack\demo\node_modules\css-loader\dist\cjs.js
// 运行结果分析, 它们的执行顺序如下:
'style-loader','postcss-loader','sass-loader', 'css-loader'
为什么style-loader会最先执行呢?
因为style-loader中有pitch, 且pitch有返回结果
// 交换'sass-loader', 'postcss-loader'的顺序,测试结果如下:
2
D:\code\web_learn\webpack\demo\index.sass
D:\code\web_learn\webpack\demo\node_modules\sass-loader\dist\cjs.js
1
D:\code\web_learn\webpack\demo\index.sass
D:\code\web_learn\webpack\demo\node_modules\postcss-loader\dist\cjs.js
0
D:\code\web_learn\webpack\demo\index.sass
D:\code\web_learn\webpack\demo\node_modules\css-loader\dist\cjs.js
// 将css-loader放置在最后,会报错
Module build failed (from ./node_modules/sass-loader/dist/cjs.js):
SassError: Expected newline.
Pitching Loader
测试1:
webpack.config.js
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'),
}
]
},
]
index.js
const index = 'index.js'
const loader1 = 'loader1.js'
const loader2 = 'loader2.js'
const loader3 = 'loader3.js'
console.log(index)
loader1.js
module.exports = function(source, options){
const str = '\n console.log(loader1)'
console.log('执行了loader1')
return source + str
}
loader2.js
module.exports = function(source, options){
const str = '\n console.log(loader2)'
console.log('执行了loader2')
return source + str
}
loader3.js
module.exports = function(source, options){
const str = '\n console.log(loader3)'
console.log('执行了loader3')
return source + str
}
打包结果index.bundle.js
const index = 'index.js'
const loader1 = 'loader1.js'
const loader2 = 'loader2.js'
const loader3 = 'loader3.js'
console.log(index)
console.log(loader3)
console.log(loader2)
console.log(loader1)
影响loader执行顺序因素之一: pitch方法的返回内容
测试2: 修改loader2.js,在loader2.js文件中增加pitch方法,并且返回内容
loader2.js
module.exports = function(source, options){
const str = '\n console.log(loader2)'
console.log('执行了loader2')
return source + str
}
module.exports.pitch = function(remainingRequest, precedingRequest, data){
console.log("执行loader2下的pitch方法")
return '123'
}
打包结果index.bundle.js
123
console.log(loader1)
影响loader执行顺序因素之二:Rule.enforce的配置
rule.enforce: pre/post/normal
这个配置也会影响loader的执行顺序
所有一个接一个地进入的 loader,都有两个阶段:
- Pitching 阶段: loader 上的 pitch 方法,按照
后置(post)、行内(inline)、普通(normal)、前置(pre)
的顺序调用。更多详细信息,请查看 Pitching Loader。 - Normal 阶段: loader 上的 常规方法,按照
前置(pre)、普通(normal)、行内(inline)、后置(post)
的顺序调用。模块源码的转换, 发生在这个阶段。
测试3:
webpack.config.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'),
}
]
},
]
执行结果 index.bundle.js
const index = 'index.js'
const loader1 = 'loader1.js'
const loader2 = 'loader2.js'
const loader3 = 'loader3.js'
console.log(index)
console.log(loader1)
console.log(loader2)
console.log(loader3)
影响loader执行顺序因素之三:inline-loader
inline-loader是除开pre, normal, post三种loader之外的另外一种loader,这种loader文档中并不建议我们自己手动加入,而是应该由其他的loader自动生成,当inline-loader加入全家桶之后loader的执行顺序如下:
- Pitching 阶段: loader 上的 pitch 方法,按照
后置(post)、行内(inline)、普通(normal)、前置(pre)
的顺序调用。更多详细信息,请查看 Pitching Loader。 - Normal 阶段: loader 上的 常规方法,按照
前置(pre)、普通(normal)、行内(inline)、后置(post)
的顺序调用。模块源码的转换, 发生在这个阶段。
待添加测试用例。。。
影响loader执行顺序因素之四:前缀 (去掉loader)
所有普通 loader 可以通过在请求中加上 !
前缀来忽略(覆盖)。
所有普通和前置 loader 可以通过在请求中加上 -!
前缀来忽略(覆盖)。
所有普通,后置和前置 loader 可以通过在请求中加上 !!
前缀来忽略(覆盖)。
测试:在index.js中引用app.js, app.js使用-!
前缀, 则app.js只用了loader3.js
index.js
import { b } from '-!./src/app.js';
打包结果 index.bundle.js
const app = 'app.js'
console.log(app)
console.log(loader3)