webpack源码之loader

前言

loader是webpack核心概念之一,webpack官网对其详细的描述。首先先简单了解下loader的概念:

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。loader 描述了 webpack 如何处理非JavaScript模块,并且在 bundle 中引入这些依赖。

在webpack中构建打包除了js文件和json文件,其他任何文件格式的文件使用import或者require加载都需要对应的loader来处理。

这是我非常感兴趣的点,loader是如何处理除了非js和json文件的。这里简单描述下webpack官网对loader本质的说明:

所谓 loader 只是一个导出为函数的 JavaScript 模块

本篇文章就是从源码层次查看源码中对于loader的处理,从之前的文章webpack源码之模块编译可以了解到进入entry开始编译的大概流程,其中对于loader的处理的逻辑就是runLoaders函数。

runLoaders函数的具体逻辑

runLoaders函数的执行逻辑主要有下面几点:

  1. 依据webpack的配置文件中loaders的配置生成对应Object
  2. 添加相关属性和方法到loaderContext对象上
  3. 执行iteratePitchingLoaders函数
生成loader Object

该部分逻辑是清晰的,具体源码如下:

loaders = loaders.map(createLoaderObject);

依据webpack配置文件中loaders的配置生成对应的关于loader信息Object对象。

iteratePitchingLoaders函数逻辑

该函数核心处理之一是loadLoader即加载loader,这里先说说该函数,后面会方便针对iteratePitchingLoaders整体逻辑的理解。loadLoader具体源码如下:

	// load loader module
	loadLoader(currentLoaderObject, function(err) {
		// 错误处理
		var fn = currentLoaderObject.pitch;
		currentLoaderObject.pitchExecuted = true;
		if(!fn) {
			return iteratePitchingLoaders(options, loaderContext, callback);
		}
		// 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);
				if(args.length > 0) {
					loaderContext.loaderIndex--;
					iterateNormalLoaders(options, loaderContext, args, callback);
				} else {
					iteratePitchingLoaders(options, loaderContext, callback);
				}
			}
		);
	});

这部分逻辑涉及到的方法loadLoader是来自loader-runner库的,其主要逻辑就是加载loader。loadLoader函数逻辑主要逻辑如下:

module.exports = function loadLoader(loader, callback) {
	if(typeof System === "object" && typeof System.import === "function") {
		System.import(loader.path)
		.catch(callback)
		.then(function(module) {
			loader.normal =
				typeof module === "function" ? module : module.default;
			loader.pitch = module.pitch;
			loader.raw = module.raw;
			if(typeof loader.normal !== "function" && typeof loader.pitch !== "function") {
				return callback(new LoaderLoadingError(
					"Module '" + loader.path + "' is not a loader (must have normal or pitch function)"
				));
			}
			callback();
		});
	} else {
		try {
			var module = require(loader.path);
		} catch(e) { // 相关错误逻辑处理 }
		// 同样的处理逻辑
	}
};

每一个loader实际上就是一个npm库,loadLoader函数的作用就是使用import动态载入或者require来同步加载其对应的入口文件。

结合file-loader看loadLoader逻辑

file-loader,其打包输出的主要文件基本上有3个:

  • cjs.js:入口文件,作为package.json中main字段值
  • index.js:实际loader逻辑处理的文件
  • options.json:相关loader配置项

当使用loadLoader来处理实际上就是:

import('本机地址/webpack-demo/node_modules/file-loader/dist/cjs.js')
require('本机地址/webpack-demo/node_modules/file-loader/dist/cjs.js')

而cjs文件的内容就是module.exports暴露相关内容:

"use strict";

const loader = require('./index');

module.exports = loader.default;
module.exports.raw = loader.raw;

在文章开始就给出了webpack对于loader的定义:

所谓 loader 只是一个导出为函数的 JavaScript 模块

webpack中使用import或require来加载loader,可见是模块。而下面逻辑则说明loader必须导出为函数:

loader.normal = typeof module === "function" ? module : module.default;
if(typeof loader.normal !== "function" && typeof loader.pitch !== "function") {
	return callback(
		new LoaderLoadingError(
			"Module '" + loader.path + "' is not a loader (must have normal or pitch function)"
		)
	);
}

loader加载后输出必须是function,否则会报错,而且这里还涉及到loader pitch的校验。

看完loadLoader的逻辑之后,知道loadLoader可以简单认为就是加载loader。下面结合loadLoader来整体看看iteratePitchingLoaders逻辑

loader 支持链式传递,能够对资源使用流水线。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript

iteratePitchingLoaders函数的就是实现上面描述的逻辑,具体源码如下:

function iteratePitchingLoaders(options, loaderContext, callback) {
	// abort after last loader(最后一个loader处理完后停止)
	if(loaderContext.loaderIndex >= loaderContext.loaders.length)
		return processResource(options, loaderContext, callback);

	var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

	// loader已加载并且pitch函数已执行
	if(currentLoaderObject.pitchExecuted) {
		loaderContext.loaderIndex++;
		return iteratePitchingLoaders(options, loaderContext, callback);
	}
	// 加载loader
	loadLoader(currentLoaderObject, function(err) {
		// 错误处理
		var fn = currentLoaderObject.pitch;
		currentLoaderObject.pitchExecuted = true;
		if(!fn) {
			return iteratePitchingLoaders(options, loaderContext, callback);
		}
		// 涉及pitch函数的处理逻辑
	});
}

iteratePitchingLoaders函数涉及递归,这里通过实例具体来分析整个流程。假设存在一组loaders(都不存在pitch函数),其定义顺序是loader-a、loader-b

其执行顺序如下:

  • 第1次执行,当前loader为loader-a,调用loadLoader函数加载,其pitch函数为null,需要递归
  • 第2次执行,需要注意当前loader还是loader-a,此时会执行loaderIndex++逻辑,需要递归
  • 第3次执行,此时是loader-b,会直接调用loadLoader函数加载,其pitch函数为null,需要递归
  • 第4次执行,需要注意当前loader还是loader-b,此时会执行loaderIndex++逻辑,需要递归
  • 第5次执行,此时loadIndex > loaders总数,执行processResource

从上面执行流程发现没有pitch函数的loader实际上都没有真正执行,仅仅都是加载而已。那么loader真正执行逻辑在哪?就是processResource函数,具体代码如下:

function processResource(options, loaderContext, callback) {
	// 一组loader从右到左执行的关键
	loaderContext.loaderIndex = loaderContext.loaders.length - 1;
	var resourcePath = loaderContext.resourcePath;
	if(resourcePath) {
		loaderContext.addDependency(resourcePath);
		options.readResource(resourcePath, function(err, buffer) {
			if(err) return callback(err);
			options.resourceBuffer = buffer;
			iterateNormalLoaders(options, loaderContext, [buffer], callback);
		});
	} else {
		iterateNormalLoaders(options, loaderContext, [null], callback);
	}
}
// iterateNormalLoaders源码
function iterateNormalLoaders(options, loaderContext, args, callback) {
	if(loaderContext.loaderIndex < 0)
		return callback(null, args);

	var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

	// iterate
	if(currentLoaderObject.normalExecuted) {
		loaderContext.loaderIndex--;
		return iterateNormalLoaders(options, loaderContext, args, callback);
	}
	// 根据上面loadLoader的逻辑可知noraml就是loader模块输出对象,即loader模块功能函数
	var fn = currentLoaderObject.normal;
	currentLoaderObject.normalExecuted = true;
	if(!fn) {
		return iterateNormalLoaders(options, loaderContext, args, callback);
	}
	runSyncOrAsync(fn, loaderContext, args, function(err) {
		if(err) return callback(err);

		var args = Array.prototype.slice.call(arguments, 1);
		iterateNormalLoaders(options, loaderContext, args, callback);
	});
}

还是以上面loader-a、loader-b实例来看,涉及processResource和iterateNormalLoaders逻辑:

  • processResource中设置loaderIndex === loaders数量
  • 第1次iterateNormalLoaders执行,此时loader是loader-b,设置其normalExecuted = true,执行runSyncOrAsync即loader模块逻辑执行,执行完毕后递归
  • 第2次iterateNormalLoaders执行,此时loader还是loader-b,满足条件normalExecuted = true,直接递归iterateNormalLoaders不执行runSyncOrAsync
  • 第3次iterateNormalLoaders执行,此时loader是loader-a,设置其normalExecuted = true,执行runSyncOrAsync即loader模块逻辑执行,执行完毕后递归
  • 第4次iterateNormalLoaders执行,此时loader是loader-a,满足条件normalExecuted = true,直接递归iterateNormalLoaders不执行runSyncOrAsync
  • 第4次iterateNormalLoaders执行,此时loaderIndex < 0,执行回调并退出

至此一组loader就全部执行完毕,webpack采用递归来完成一组loader从后至前执行过程,其核心在于loaderIndex值的控制。实际上如果loader存在pitch函数其整体执行逻辑会存在些许差异,这个关键在于loadLoader回调函数中。

涉及pitch函数

loadLoader对应的回调函数关于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);
		if(args.length > 0) {
			loaderContext.loaderIndex--;
			iterateNormalLoaders(options, loaderContext, args, callback);
		} else {
			iteratePitchingLoaders(options, loaderContext, callback);
		}
	}
);

上面源码中fn就是pitch函数,对于存在pitch函数的loader会直接调用runSyncOrAsync,并根据返回值来做相关处理(如果某一个loader的pitch函数存在额外的返回值就会导致当前及后续loader的pitch函数以及功能逻辑都不会执行,还是比较危险的操作),其执行逻辑基本与之前无pitch函数过程类似,都是递归处理,这里就不再深入。

loader处理存在pitch阶段和normal阶段,假设一组loader都存在pitch函数,首先是从左到右依次执行其pitch函数,然后再从右到左执行loader的功能逻辑

loader转换对象确定

从之前的文章webpack源码之模块编译实际上可以知道webpack的依赖收集是在解析阶段(loader转换之后)。这里就会思考:loader是如何明确自己需要转换的文件的。
实际上回答这个问题之前,就要明确webpack中什么是依赖:

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件

这是webpack官网对loader的说明,从这里可以间接知道loader的操作对象是什么?

loader的操作对象是所有通过import或require等命令加载的模块文件

这里就明确了,依赖就是模块中所有import或require等命令加载的模块。模块的依赖收集是在其loader转换之后,那么自然而然可以想到的处理逻辑就是:

当模块依赖收集完毕后,就会对依赖模块文件应用loader的匹配规则,看看是否满足loader的处理条件,而同一组loader在webpack源码中会属于同一个loaderContext。

这里简单描述下存在依赖的处理流程:

  • 模块A执行完loader转换、acorn解析和依赖收集(假设为依赖1、依赖2)
  • 对依赖进行逐个处理,依赖1进行loader处理、acorn解析和依赖收集(此时自然而然建立loader与模块之间转换关系)、之后进行依赖2进行loader转换、acorn解析和依赖收集
  • 所有模块处理完成后输出最终文件

总结

webpack中任何文件都是一个模块,在源码内部对应每一个模块都会一个module对象,而每一个module对象都会通过runLoaders来处理文件转换。

通过源码加深了对loader的一些特性的理解:

  • loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。这个过程在webpack源码中是通过递归来实现。
  • loader处理过程分为pitch阶段和normal阶段,pitch阶段处理loader的pitch函数,normal阶段才是真正的loader功能的执行
  • 对于一组loader,pitch阶段按照从左到右依次执行loader的pitch函数,normal结算按照从右到左依次实现loader的转换功能
  • loader本质上就是一个必须输出函数的模块,webpack内部也是通过import或require等命令来实现loader的加载
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值