# 本文介绍loader的大纲
- 概念: 何谓 loader,从基础开发快速入门日用配置
- 原理: 从源码角度解读,手把手实现核心
loader-runner库 - 实现: 复刻高频出现
babel-loader,掌握loader开发流程
# 一、loader 概念
# loader 的作用
loader本质是个函数,将 webpack 不能识别的资源文件转化为 js。
webpack中通过compilation对象进行模块的编译,首先进行匹配loader处理文件得到的结果(string|buffer),之后才会输出给 webpack 进行编译。
简单来说,通过它可以在webpack处理特定的资源(文件)之前进行提前处理。
如 :ts 文件通过babel-loader处理成 js,之后再交给 webpack 处理。
# loader 配置相关 api
# 常见基础配置参数
# webpack 配置配置 loader 的三种方式。
# 绝对路径
# resolveLoader.alias
# resolveLoader.module
# loader 种类和执行顺序
种类:
- preLoader
- normalLoader
- inlineLoader
- postLoader
执行阶段
- pitch 阶段
- normal 阶段
# loader 的 pitch 阶段
# pitch loader 的熔断效果
pitch 是 loader 中的一个函数
loader.pitch = function () {
//若不return undefined会发生熔断。值返回到normal阶段对于的loader中
return "xxxxstring";
};
# loader 开发的相关 api
# 执行顺序对 loader 开发的影响
执行顺序仅仅取决于 webpack 应用 loader 时的配置(或者引入文件时候添加的前缀)。
# 同步 or 异步 loader
异步 loader
//返回promise
function asyncLoader() {
return Promise((resolve) => {
setTimeout(() => resolve("xxxstring"), 10000);
});
}
//通过this.async
function asyncLoader() {
const callback = this.async();
//do something
// ....
// 告诉loader-runner异步loader结束
callback("xxxstring");
}
# normal loader
// source为需要处理的源文件内容
function loader(source) {
//本次处理返回的内容
return source + "hello!";
}
//ps : 函数的this中挂载很多属性 如(this.getOptions()获取loader的配置 )
# pitch loader
传入三个参数
- remainingRequest
- previousRequest
- data
// normal loader
function loader(source) {
// ...
return source;
}
// pitch loader
loader.pitch = function (remainingRequest, previousRequest, data) {
// ...
};
remainingRequest
表示剩余需要处理的loader的绝对路径以!分割组成的字符串
previousRequest
表示 pitch 阶段已经迭代过的loader按照!分割组成的字符串
data
默认是一个空对象{},normalLoader和pitchLoader 通过 data 交互
function loader(source) {
log(this.data.name); //'xxxstring'
return source;
}
loader.pitch = function (remainingRequest, previousRequest, data) {
data.name = "xxxstring";
};
# loader 的 raw 属性
标志返回的值是buffer还是string
function loader2(source) {
// 此时source是一个Buffer类型 而非模型的string类型
}
loader2.raw = true;
module.exports = loader2;
# 返回值
normal阶段 loader 的返回值会在loader chain进行层层传递,最后的返回值传给webpack(一个module的代码)
pitch阶段 任意一个pitch loader非undefined 会发生熔断。理解为掉头输出了。
# loader 源码解析
创建步骤
如上图创建对应文件
- loader 文件夹存放对应的 loader(其他自行类比创建)
//line1-loader.js
function loader(source) {
console.log("inline1: normal", source);
return source + "//inline1";
}
loader.pitch = function () {
console.log("inline1 pitch");
};
module.exports = loader;
- index.js(入口文件)
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
// 模块路径
const filePath = path.resolve(__dirname, "./title.js");
// 模拟模块内容和.title.js一模一样的内容
const request = "inline1-loader!./title.js";
const rules = [
// 普通loader
{
test: /\.js$/,
use: ["normal1-loader"],
},
// 前置loader
{
test: /\.js$/,
use: ["pre1-loader"],
enforce: "pre",
},
// 后置loader
{
test: /\.js$/,
use: ["post1-loader"],
},
];
//提取inline-loader
const parts = request.replace(/^-?!+/, "").split("!");
//获取文件路径
const sourcePath = parts.pop(); // ./title.js
// inline-loader
const inlineLoaders = parts;
const preLoaders = [],
normalLoaders = [],
postLoaders = [];
rules.forEach((item) => {
if (item.test.test(sourcePath)) {
switch (item.enforce) {
case "pre":
preLoaders.push(...item.use);
break;
case "post":
postLoader.push(...item.use);
break;
default:
normalLoaders.push(...item.use);
break;
}
}
});
/*
最终过滤
! 排除 normal
!! 仅剩下 inline-loader
-! 剩下post 和 normal
*/
let loaders = [];
if (request.startsWith("!")) {
loaders.push(...postLoaders, ...inlineLoaders, ...preLoaders);
} else if (request.startsWith("!!")) {
loaders.push(...inlineLoaders);
} else if (request.startsWith("-!")) {
loaders.push(...postLoaders, ...normalLoaders);
} else {
loaders.push(
...postLoaders,
...inlineLoaders,
...preLoaders,
...normalLoaders
);
}
const resolveLoader = (pathName) =>
path.resolve(__dirname, "./loader", pathName);
loaders = loaders.map(resolveLoader);
//使用官方的
runLoaders(
{
resources: filePath,
loaders,
context: { name: "xxxstring" }, //传递的上下文对象
readResource: fs.readFile.bind(fs),
//processResource 暂时忽略
},
(error, result) => {
console.log(error, "存在的错误");
console.log(result, "结果");
/*
result.result 是一个数组 表示本次经过所有loader处理完毕的文件内容
result.resourceBuffer: buffer 原始内容转化为的buffer结果
*/
}
);
去对应文件夹下,执行node index.js。结果如下:

- title.js(真正的模块内容)
require("inline1-loader!inline2-loader!./title.js");
//其他语句就不写了,为了入口文件模拟模块内容简洁一点。
- core 文件夹(实现自己的
runLoader方法) 核心逻辑是接收待处理的资源文件路径,根据传入的 loader,首先经过pitch阶段读取资源内容再经过normal阶段处理资源内容最终得到返回结果
!源码实现!
//loader-runner
const fs = require("fs");
//1 入口
function runLoaders(options, callback) {
// 需要处理的资源绝对路径
const resource = options.resource || "";
// 需要处理的所有loaders 组成的绝对路径数组
let loaders = options.loaders || [];
// loader执行上下文对象 每个loader中的this就会指向这个loaderContext
const context = options.context || {};
// 读取资源内容的方法
const readResource = options.readResource || fs.readFile.bind(fs);
// 根据loaders路径数组创建loaders对象
loaders = loaders.map(createLoaderObject);
}
// 2 loader数组转华为loader对象
function createLoaderObject(loader) {
const obj = {
normal: null, //loader normal函数自身
pitch: null, //
raw: null, // 传过来的source是buffer还是string
data: null,
pitchExecuted: false, //pitch是否执行过
normalExecuted: false, //normal是否执行过
request: loader, //保存当前loader资源绝对路径
};
//真实源码还支持esm
const normalLoader = require(obj.request);
//赋值阶段
obj.normal = normalLoader;
obj.pitch = normalLoader.pitch;
obj.raw = normalLoader.raw;
return obj;
}
3 回到 runLoader 中 赋值loaderContext
loaderContext.resourcePath = resource; //资源路径绝对地址
loaderContext.readResource = readResource; //读取该资源的方法
loaderContext.loaderIndex = 0; //执行对应的loader idx表示还没执行过
loaderContext.loaders = loaders; //所有的loader对象
loaderContext.data = null;
//标志异步loader的对象属性
loaderContext.async = null;
loaderContext.callback = null;
//request 拼接所有loader+资源路径 eg: !inline-loader!pre-loader./title.js
Object.defineProperty(loaderContext, "request", {
enumerable: true,
get() {
loaderContext.loaders
.map((l) => l.request)
.concat(loaderContext.resourcePath || "")
.join("!");
},
});
// remainingRequest
Object.defineProperty(loaderContext, "remainingRequest", {
enumerable: true,
get: function () {
return loaderContext.loaders
.slice(loaderContext.loaderIndex + 1)
.map((l) => l.request)
.join("!");
},
});
// currentRequest (包括自身)
Object.defineProperty(loaderContext, "previousRequest", {
enumerable: true,
get: function () {
return loaderContext.loaders
.slice(0, loaderContext.loaderIndex+1)
.map((l) => l.request)
.join("!");
},
});
// previousRequest
Object.defineProperty(loaderContext, "previousRequest", {
enumerable: true,
get: function () {
return loaderContext.loaders
.slice(0, loaderContext.loaderIndex)
.map((l) => l.request)
.join("!");
},
});
// 上下文的data idx到第几个就是使用第几个的data
Object.defineProperty(loaderContext, "data", {
enumerable: true,
get: function () {
return loaderContext.loader[loaderContext.loaderIndex].data
},
});
迭代pitchloader
function runLoaders(options,callback){
...
// 存储读取资源文件的二进制内容 (转化前原始内容)
const processOptions = {
resourceBuffer: null,
};
//迭代pitch
iteratePitchingLoaders(processOptions,loaderContext,(err,result)=>{
callback(err,{result,resourceBuffer:processOptions.resourceBuffer})
})
}
迭代pitch的方法
/**
* 迭代pitch-loaders
* 核心思路: 执行第一个loader的pitch 依次迭代 如果到了最后一个结束 就开始读取文件
* @param {*} options processOptions对象
* @param {*} loaderContext loader中的this对象
* @param {*} callback runLoaders中的callback函数
*/
function iteratePitchingLoaders(options, loaderContext, callback) {
// 超出loader个数 表示所有pitch已经结束 那么此时需要开始读取资源文件内容
if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
return processResource(options, loaderContext, callback);
}
const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
// 当前loader的pitch已经执行过了 继续递归执行下一个
if (currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchingLoaders(options, loaderContext, callback);
}
const pitchFunction = currentLoaderObject.pitch;
// 标记当前loader pitch已经执行过
currentLoaderObject.pitchExecuted = true;
// 如果当前loader不存在pitch阶段
if (!currentLoaderObject.pitch) {
return iteratePitchingLoaders(options, loaderContext, callback);
}
// 存在pitch阶段 并且当前pitch loader也未执行过 调用loader的pitch函数
runSyncOrAsync(
pitchFunction,
loaderContext,
[
currentLoaderObject.remainingRequest,
currentLoaderObject.previousRequest,
currentLoaderObject.data,
],
function (err, ...args) {
if (err) {
// 存在错误直接调用callback 表示runLoaders执行完毕
return callback(err);
}
// 根据返回值 判断是否需要熔断 or 继续往下执行下一个pitch
// pitch函数存在返回值 -> 进行熔断 掉头执行normal-loader
// pitch函数不存在返回值 -> 继续迭代下一个 iteratePitchLoader
const hasArg = args.some((i) => i !== undefined);
if (hasArg) {
loaderContext.loaderIndex--;
// 熔断 直接返回调用normal-loader
iterateNormalLoaders(options, loaderContext, args, callback);
} else {
// 这个pitch-loader执行完毕后 继续调用下一个loader
iteratePitchingLoaders(options, loaderContext, callback);
}
}
);
}
执行pitch loader的方法
/**
*
* 执行loader 同步/异步
* @param {*} fn 需要被执行的函数
* @param {*} context loader的上下文对象
* @param {*} args [remainingRequest,previousRequest,currentLoaderObj.data = {}]
* @param {*} callback 外部传入的callback (runLoaders方法的形参)
*/
function runSyncOrAsync(fn, context, args, callback) {
// 是否同步 默认同步loader 表示当前loader执行完自动依次迭代执行
let isSync = true;
// 表示传入的fn是否已经执行过了 用来标记重复执行
let isDone = false;
// 定义 this.callback
// 同时this.async 通过闭包访问调用innerCallback 表示异步loader执行完毕
const innerCallback = (context.callback = function () {
isDone = true;
// 当调用this.callback时 标记不走loader函数的return了
isSync = false;
callback(null, ...arguments);
});
// 定义异步 this.async
// 每次loader调用都会执行runSyncOrAsync都会重新定义一个context.async方法
context.async = function () {
isSync = false; // 将本次同步变更成为异步
return innerCallback;
};
// 调用pitch-loader执行 将this传递成为loaderContext 同时传递三个参数
// 返回pitch函数的返回值 甄别是否进行熔断
const result = fn.apply(context, args);
if (isSync) {
isDone = true;
if (result === undefined) {
return callback();
}
// 如果 loader返回的是一个Promise 异步loader
if (
result &&
typeof result === 'object' &&
typeof result.then === 'function'
) {
// 同样等待Promise结束后直接熔断 否则Reject 直接callback错误
return result.then((r) => callback(null, r), callback);
}
// 非Promise 切存在执行结果 进行熔断
return callback(null, result);
}
}
结束遍历pitch loader ,执行 pitch loader后,读取资源文件的内容了
function processResource(options, loaderContext, callback) {
// 重置越界的 loaderContext.loaderIndex
// 达到倒叙执行 pre -> normal -> inline -> post
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
const resource = loaderContext.resourcePath;
// 读取文件内容
loaderContext.readResource(resource, (err, buffer) => {
if (err) {
return callback(err);
}
// 保存原始文件内容的buffer 相当于processOptions.resourceBuffer = buffer
options.resourceBuffer = buffer;
// 同时将读取到的文件内容传入iterateNormalLoaders 进行迭代`normal loader`
iterateNormalLoaders(options, loaderContext, [buffer], callback);
});
}
边读边执行文件
/**
* 迭代normal-loaders 根据loaderIndex的值进行迭代
* 核心思路: 迭代完成pitch-loader之后 读取文件 迭代执行normal-loader
* 或者在pitch-loader中存在返回值 熔断执行normal-loader
* @param {*} options processOptions对象
* @param {*} loaderContext loader中的this对象
* @param {*} args [buffer/any]
* 当pitch阶段不存在返回值时 此时为即将处理的资源文件
* 当pitch阶段存在返回值时 此时为pitch阶段的返回值
* @param {*} callback runLoaders中的callback函数
*/
function iterateNormalLoaders(options, loaderContext, args, callback) {
// 越界元素判断 越界表示所有normal loader处理完毕 直接调用callback返回
if (loaderContext.loaderIndex < 0) {
return callback(null, args);
}
const currentLoader = loaderContext.loaders[loaderContext.loaderIndex];
if (currentLoader.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, args, callback);
}
const normalFunction = currentLoader.normal;
// 标记为执行过
currentLoader.normalExecuted = true;
// 检查是否执行过
if (!normalFunction) {
return iterateNormalLoaders(options, loaderContext, args, callback);
}
// 根据loader中raw的值 格式化source
convertArgs(args, currentLoader.raw);
// 执行loader
runSyncOrAsync(normalFunction, loaderContext, args, (err, ...args) => {
if (err) {
return callback(err);
}
// 继续迭代 注意这里的args是处理过后的args
iterateNormalLoaders(options, loaderContext, args, callback);
});
}
# 总结 run-loader
- loader绝对路径 --> loaders对象
- 生成上下文对象
- 遍历
pitch-loader执行 pitch-loader - 遍历完重置, 读取资源
执行 normal-loader