Webpack-loader的执行顺序

1,328 阅读4分钟

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,都有两个阶段:

  1. Pitching 阶段: loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。更多详细信息,请查看 Pitching Loader
  2. 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的执行顺序如下:

  1. Pitching 阶段: loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。更多详细信息,请查看 Pitching Loader
  2. 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)