前言
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函数的执行逻辑主要有下面几点:
- 依据webpack的配置文件中loaders的配置生成对应Object
- 添加相关属性和方法到loaderContext对象上
- 执行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的加载