1. loader的分类及运行机制
1. 每个loader都是一个函数,一个loader分为normal-loader 和pitch-loader(此loader可以不实现), 我们都知道webpack.config.js文件中配置的loader是从下往上, 从右往左执行, 这说的是normal-loader的执行顺序, 而pitch-loader的顺序正好与normal-loader相反, 所以loader的执行过程是pitch-loader =》 读取文件源码 =》 normal-loader。当pitch-loader有返回值时, 会省略读源码及此后的normal-loader。
2. loader执行机制示意图
-
- loader 函数
function loader(inputSource) {
console.log("normal-loader")
return inputSource
}
loader.pitch = function(){
console.log("pitch-loader")
}
// 当设置为true loader输出的内容为buffer
loader.raw = true;
module.export = loader
-
-
pitch-loader 无返回的示意图
- 2. pitch-loader 有返回值的示意图
当pitch2-loader有返回值时 loader会舍弃自身的和自身之后的normal-loader, 直接执行上一个normal-loader
-
2. loader的运行的实现逻辑
1. 先实现一个runLoader函数, 传入参数options和执行完所有loader后的回调函数
const path = require("path");
const fs = require("fs");
const readFile = fs.readFileSync;
// 配置文件, 相当于webpack.config.js
let options = {
// 打包的文件入口
resource: path.resolve(__dirname, "./src/title.js"),
// 需要执行的所有loader
loaders: [
path.resolve(__dirname, "./loaders/a-loader.js"),
path.resolve(__dirname, "./loaders/b-loader.js"),
path.resolve(__dirname, "./loaders/c-loader.js"),
]
}
// 运行loader的方法
function runLoader (options, finalCallBack) {}
runLoader(options, (err, result) => {
console.log(result)
})
2. 先创建loader执行的上下文环境loaderContxt 及存储入口文件 loader等, 对于loader需要将每个loader名转变为一个loader对象的形式, 存储loader的路径,pitch-loader和normal-loader公用数据data(调用函数createLoader转换创建)
function createLoader (path) {
// loader对象
let loaderObject = { data: {} };
// loader 路径
loaderObject.path = path;
// normal=loader
loaderObject.normal = require(path);
//pitch-loader
loaderObject.pitch = loaderObject.normal.pitch;
return loaderObject;
}
function runLoader (options, finalCallBack) {
// loader的上下文环境,loader执行时会将this指向此loaderContxt
let loaderContxt = {};
//打包的入口文件
const resource = options.resource;
let loaders = options.loaders;
// 处理loaders, 进行loader对象转换
loaders = loaders.map(createLoader);
// 执行的loader序列,用于判断normal和pitch的执行
loaderContxt.loaderIndex = 0;
// 存入上下文打包入口路径
loaderContxt.resource = resource;
// 存入上下文读取文件内容方法
loaderContxt.readFile = readFile;
// 所有loader对象存入上下文
loaderContxt.loaders = loaders;
}
3.执行pitch-loader
iteratePitchingLoaders(loaderContxt, finalCallBack);
function iteratePitchingLoaders (loaderContxt, finalCallBack) {
}
-
iteratePitchingLoaders 内部实现逻辑
- 首先要判断当前loader的loaderIndex是否大于所有需要执行loader的长度, 如果超出则需要调用processSource去读当前要处理的文件源码, 源码读取完会执行iterateNormalLoader调用normal-loader
- 取出当前的loader对象并获取pitch-loader函数, 如果loader定义了pitch-loader函数,并且pitch-loader有返回值, 那就直接调用上一个normal-loader, 否则 loaderIndex+1 执行iteratePitchingLoaders查找下一个
- pitch-loader函数函数执行时需要传入参数剩余的未执行的loader(remainingRequest),已经执行的loader(previousRequest) 和pitch-loader和normal-loader的共享数据对象data, 比如我们options中配置的loader 如果已经执行了a-loader, 那么remainingRequest就是 b-loader.js!c-loader.js!title.js, 而previousRequest为a-loader.js。 data为我们在normal-loader或pitch-loader对this.loader赋值的数据。 remainingRequest、previousRequest、data 我们需要用Object.definePProperty去动态获取
// 所有的loader
Object.defineProperty(loaderContxt, "request", {
get () {
return loaderContxt.loaders.map(loader => loader.path)
.concat(loaderContxt.resource).join("!");
}
})
// 剩余未执行的loader
Object.defineProperty(loaderContxt, "remainingRequest", {
get () {
return loaderContxt.loaders.slice(loaderContxt.loaderIndex + 1).map(loader => loader.path)
.concat(loaderContxt.resource).join("!");
}
})
// 已经执行的loader
Object.defineProperty(loaderContxt, "previousRequest", {
get () {
return loaderContxt.loaders.slice(0, loaderContxt.loaderIndex).map(loader => loader.path).join("!");
}
})
// 当前loader的data, 因为loader内可以用this.data获取
Object.defineProperty(loaderContxt, "data", {
get () {
return loaderContxt.loaders[loaderContxt.loaderIndex].data;
}
})
function iteratePitchingLoaders (loaderContxt, finalCallBack) {
// 如果当前的loaderIndex已经大于或超出loader的length,就直接去读取文件源码
if (loaderContxt.loaderIndex >= loaderContxt.loaders.length) {
// 需要回到loader的最后一个
loaderContxt.loaderIndex--;
return processSource(loaderContxt, finalCallBack);
}
// 从loaders中取出当前loader 的pitch
const currentLoaderObject = loaderContxt.loaders[loaderContxt.loaderIndex];
const pitchFn = currentLoaderObject.pitch;
let res;
if (pitchFn && (res = pitchFn.call(loaderContxt,
loaderContxt.remainingRequest, loaderContxt.previousRequest, loaderContxt.data))) {
// 如果此loader定义了pitch就执行pitch函数
// 如果有返回值,直接跳过剩下loader的pitch和source,和当前loader的normal,往回执行normalloader
loaderContxt.loaderIndex--;
return iterateNormalLoader(loaderContxt, res, finalCallBack);
} else {
// 未定义loader就寻找下一个loader是否定义pitch函数
loaderContxt.loaderIndex++;
iteratePitchingLoaders(loaderContxt, finalCallBack);
}
}
// 读取文件源码
function processSource (loaderContxt, finalCallBack) {
const source = loaderContxt.readFile(loaderContxt.resource);
iterateNormalLoader(loaderContxt, source, finalCallBack);
}
4. 调用iterateNormalLoader执行normal-loader
- iteratePitchingLoaders 内部实现逻辑
- 如果loader的loaderIndex小于0, 直接执行finalCallBack函数 2.取出normal-loader, 因为raw参数决定loader的输出内容是utf8格式还是buffer, 所以需要执行convertSource对内容进行格式转化 3.同步执行normal-loader或异步执行normal-loader,用户可以通过this.async函数获取主动调取normal-loader的控制权, 同步和异步的不同逻辑在于, 同步逻辑,是一个loader执行完自动调用iterateNormalLoader执行下一个loader,异步知性逻辑是将调用iterateNormalLoader的执行时机交给用户
- 代码实现
// 源码转换内容格式
function convertSource (source, raw) {
if (raw && !Buffer.isBuffer(source)) {
// 如果想要buffer类型,source不是buffer就需要转换
source = new Buffer(source,)
} else if (!raw && Buffer.isBuffer(source)) {
// 如果是buffer想要字符串类型,转成字符串
source = source.toString("utf8");
}
// 其他不处理直接返回
return source;
}
// 根据pitch再决定需不需要执行normalLoader
function iterateNormalLoader (loaderContxt, source, finalCallBack) {
let result;
// 如果loaders中的loader已经执行完, 执行finalCallBack
if (loaderContxt.loaderIndex < 0) {
return finalCallBack(null, source);
}
// 取出当前的loader
const currentLoaderObject = loaderContxt.loaders[loaderContxt.loaderIndex];
const normalFn = currentLoaderObject.normal;
// 因为在loader函数中可以用raw设置内容是二进制还是字符串首先要判断当前的
// loader是输出二进制聂荣还是字符串内容
source = convertSource(source, normalFn.raw);
runAsyncOrSyncLoader(normalFn, loaderContxt, source, finalCallBack);
}
function runAsyncOrSyncLoader (normalFn, loaderContxt, source, finalCallBack) {
// 默认是同步执行
let isSync = true;
// 提供给使用者的函数
const innerCallback = loaderContxt.callback = function (err, source) {
// /执行完一个loader后就重置为同步执行
iterateNormalLoader(loaderContxt, source, finalCallBack);
}
// 改变loader为异步获取结果, 并将继续执行下一个loader的控制权移交给用户
loaderContxt.async = function () {
isSync = false
return innerCallback;
}
loaderContxt.loaderIndex--;
// 不管同步异步获取结果都要执行loader
let result = normalFn.call(loaderContxt, source,);
// 如果是同步loader代码, 就继续调用iterateNormalLoader调用下一个loader
if (isSync) {
iterateNormalLoader(loaderContxt, result || source, finalCallBack);
}
}