「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
在了解这个问题之前,我们先了解一点关于loader的基础知识
不论什么模块,经过Loader的处理之后都会变成JS模块
最左侧的loader返回值必须是JS模块,不然webpack无法识别其他语言模块(webpack只认识JS和JSON)
1. Loader的类型
loader分为四类:loader的叠加顺序是 后置(post) + 内联(inline) + 正常(nomore) + 前置(pre)
如何指定loader的类型
- 通过enforce: 'post'/'pre' 来指定类型,如果不写就是nomore类型
- inline-loade需要写在行内
rules: [
// 正常
{
test: /.js/,
use: [
path.resolve(__dirname, 'loaders', 'nomore-loader.js') // loader的类型与文件名无关
]
},
// 前置
{
test: /.js/,
enforce: 'pre',
use: [
path.resolve(__dirname, 'loaders', 'pre-loader.js') // loader的类型与文件名无关
]
},
// 后置
{
test: /.js/,
enforce: 'post',
use: [
path.resolve(__dirname, 'loaders', 'post-loader.js') // loader的类型与文件名无关
]
}
]
// index.js
// !是分隔符,用来区分loader和loader之间和文件之间的区别
const title = require('inline-loader!./title');
2. Loader的执行顺序
4 post loader <= 3 inline loader <= 2 nomore loader <= 1 pre loader
会先执行pre loader, nomore loade, inline loader, post loader
这个顺序就是我们所说的从右往左执行
2.1 pitch
那什么是pitch?
每个Loader 都有两个函数,一个是nomore,一个是pitch
pitch是loader上的一个属性,也是一个函数,并且pitch的执行顺序是从左向右执行,可以看下图
如果pitch的函数有返回值,就会默认后面的loader都执行完毕,就会执行上一个loader的loader函数,且返回值会作为上一个loader函数的参数
会先执行post-loader的pitch方法,如果没有返回值就会执行下一个inline-loader的pitch方法,如果还没有返回值就继续执行,直到读取文件之后,从后向前的执行nomore, 也就是上面的loader函数,如果pitch有返回值就会默认认为后面所有的pitch和nomore都执行了,就会执行前一个的nomore函数
3. loader的特殊配置
3.1 !noAutoLoaders
不要普通loader 也就是nomore
3.2 -! noPreAutoLoaders
不要前置loader和普通loader,也就是pre-loader和nomore-loader
3.3 !! noPrePostAutoLoaders
不要前置后置普通loader,只要行内loader,也就是只要inline-loader
4. Loader-Runner
loader是如何执行的
const { runLoaders } = require('loader-runner');
const path = require('path');
const fs = require('fs');
// 入口文件
const enrtyFile = path.resolve(__dirname, 'src', 'title.js');
// 规则
const rules = [
{
test: /title.js$/,
use: ["nomore-loader.js"]
},
{
test: /title.js$/,
enforce: 'post',
use: ["post-loader.js"]
},
{
test: /title.js$/,
enforce: 'pre',
use: ["pre-loader.js"]
}
];
/**
* -! noPreAutoLoaders 不要前置和普通loader
* ! noAutoLoaders 不要普通loader
* !! noPrePostAutoLoaders 不要前置后置普通loader,只要inline loader
*/
let request = `!!inline-loader!${enrtyFile}`;
let parts = request.replace(/^-?!+/, '').split('!'); // [inline-loader, enrtyFile];
let resource = parts.pop(); //入口文件
const inlineLoaders = parts;
const preLoaders = [],
postLoaders = [],
nomoreLoaders = [];
rules.forEach(rule => {
if (rule.test.test(resource)) {
if (rule.enforce === 'pre') {
preLoaders.push(...rule.use)
} else if (rule.enforce === 'post') {
postLoaders.push(...rule.use)
} else {
nomoreLoaders.push(...rule.use)
}
}
});
let loaders = [];
if (request.startsWith('!!')) {
loaders = [...inlineLoaders];
} else if (request.startsWith('!')) {
loaders = [...postLoaders, ...inlineLoaders, ...preLoaders];
} else if (request.startsWith('-!')) {
loaders = [...postLoaders, ...inlineLoaders];
} else {
loaders = [...postLoaders, ...inlineLoaders, ...nomoreLoaders, ...preLoaders];
};
// 把相对路径转换成绝对路径
const resolveLoader = loader => path.resolve(__dirname, 'loader', loader);
loaders = loaders.map(resolveLoader);
// 执行
runLoaders({
resource,
loaders,
context: {name: 'test loader'},
readResource: fs.readFile.bind(fs)
}, (err, result) => {
console.log('err', err);
console.log('result', result);
// 转换前的源文件的内容
console.log('buffer', result.resourceBuffer.toString('utf8'));
})
5. Babel-loader
- babel-loader 只是一个函数,用来转换JS源代码的函数,具体的转换规则用core来实现
- @babel/core babel的核心包,用来把源代码转换成ast抽象语法树,在把语法树转换成新的源代码
- babel plugin babel/core只是用来转换代码,但是不做具体的处理,插件是用来做具体的事情,比如把箭头函数转换成普通函数等
- @bebel/preset-env 插件的预设,相当于把常用的很多插件都打包到了一起
const babel = require('@babel/core');
// loader 都是一个函数,参数是来源,源代码
function loader(source) {
// options用来配置用什么插件来做什么功能等
const options = this.getOptions({});
// 通过core进行转换,把新生成的源代码进行返回
let { code } = babel.transform(source, options);
return code
}
options: {
presets: ["@babel/preset-env"]
}
6. 实现 RunLoaders
总结流程为
- 执行loader-runner,获取所有loaders,并且转换成绝对路径,合并其他参数传入
- 执行runLoaders方法,传入合并后的参数和回调
- 开始执行iteratePitchingLoaders,从左向右执行,如果遇到返回值就终止后面的loader,直接执行前一个loader函数
- 所有pitch执行完成之后,读取文件资源,然后从右向左的开始执行loader函数
- 开始执行iterateNomoreLoaders,从右向左开始,执行完成后,调用最开始传入runLoaders的回调函数,获取转换后的资源文件
// 参考 4. Loader-Runner 最后的方法执行
runLoaders({
resource,
loaders,
context: {name: 'test loader'},
readResource: fs.readFile.bind(fs)
}, (err, result) => {
console.log('err', err);
console.log('result', result);
// 转换前的源文件的内容
console.log('buffer', result.resourceBuffer.toString('utf8'));
})
// runLoader.js
const fs = require('fs');
// 把loader绝对路径变成一个loader对象
function createLoaderObject(loader) {
let nomore = require(loader);
let pitch = nomore.pitch;
let raw = nomore.raw || false;
return {
path: loader,
nomore,
pitch,
raw, // 如果是false代表文件的源代码是字符串形势,如果是true就是buffer的形势
pitchExcuted: false, // 代表当前loader的pitch是否执行过了,false是没有执行,true是执行过了
nomoreExcuted: false // 代表当前的loader的nomore是否执行过了,false是没有执行,true是执行过了
}
};
function runLoaders(options, callback) {
const { resource, loaders = [], context = {}, readResource = fs.readFile } = options;
let loaderObject = loaders.map(createLoaderObject);
let loaderContext = context;
loaderContext.resource = resource; // 要加载的资源
loaderContext.loaders = loaderObject; // loader数组对象
loaderContext.readResource = readResource; // 读取资源的方法
loaderContext.loaderIndex = 0; // 当前loader的索引
//所有的loader加上resouce
Object.defineProperty(loaderContext, 'request', {
get() {
return loaderContext.loaders.map(loader => loader.path).concat(loaderContext.resource).join('!')
}
});
//从当前的loader下一个开始一直到结束 ,加上要加载的资源
Object.defineProperty(loaderContext, 'remainingRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(loader => loader.path).concat(loaderContext.resource).join('!')
}
});
//从当前的loader开始一直到结束 ,加上要加载的资源
Object.defineProperty(loaderContext, 'currentRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex).map(loader => loader.path).concat(loaderContext.resource).join('!')
}
});
//从第一个到当前的loader的前一个
Object.defineProperty(loaderContext, 'previousRequest', {
get() {
return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(loader => loader.path).concat(loaderContext.resource).join('!')
}
});
// args:值 raw:loader是否需要buffer
function convertArgs(args, raw) {
if (raw && !Buffer.isBuffer(args[0])) {
args[0] = Buffer.from(args[0]);
} else if (!raw && Buffer.isBuffer(args[0])){
args[0] = args[0].toString();
}
}
function runSyncOrAsync(fn, loaderContext, args, runCallback) {
let isSync = true;
const callback = (...args) => {
runCallback(...args);
};
loaderContext.callback = callback;
loaderContext.async = () => {
isSync = true;
return callback;
};
let result = fn.apply(loaderContext, args);
if (isSync) {
callback(null, result)
}
};
// 开始从右向左开始执行loader方法
function iterateNomoreLoaders(options, context, args, pitchingCallback) {
// 判断边界值
if (context.loaderIndex < 0) {
return pitchingCallback(null, ...args)
}
// 取出当前执行的loader
let currentLoader = context.loaders[context.loaderIndex];
// 如果当前loader执行过
if (currentLoader.nomoreExcuted) {
context.loaderIndex--;
return iterateNomoreLoaders(options, context, args, pitchingCallback);
};
// 取出当前loader的函数
let nomoreFn = currentLoader.nomore;
currentLoader.nomoreExcuted = true;
convertArgs(args, currentLoader.raw);
runSyncOrAsync(nomoreFn, context, args, (err, ...returnArgs) => {
if (err) return pitchingCallback(err);
iterateNomoreLoaders(options, context, returnArgs, pitchingCallback)
});
}
// 读取文件
function processResource (options, context, pitchingCallback) {
options.readResource(context.resource, (err, resourceBuffer) => {
options.resourceBuffer = resourceBuffer;
// loader开始从右向左开始执行
context.loaderIndex--;
iterateNomoreLoaders(options, context, [resourceBuffer], pitchingCallback)
})
};
// 先从左向右执行pitch方法
function iteratePitchingLoaders (options, context, pitchingCallback) {
// 判断边界值,如果当前索引大于等于loader数组的长度,就代表pitch执行完毕了,开始读取文件
if (context.loaderIndex >= context.loaders.length) {
return processResource(options, context, pitchingCallback)
}
// 获取当前执行的loader
let currentLoader = context.loaders[context.loaderIndex];
// 如果true 代表已经执行过当前pitch了,索引++向后执行
if (currentLoader.pitchExcuted) {
context.loaderIndex++;
return iteratePitchingLoaders(options, context, pitchingCallback);
};
let fn = currentLoader.pitch;
currentLoader.pitchExcuted = true;//表示当前的loader的pitch已经处理过
// 如果没有pitch方法,也标记成true,执行下一个loader的pitch
if (!fn) {
currentLoader.pitchExcuted = true;
// 递归执行下一个loader
return iteratePitchingLoaders(options, context, pitchingCallback);
}
// pitch可以是同步执行或者异步执行
runSyncOrAsync(currentLoader.pitch, context, [], (err, returnArgs) => {
// 如果有返回值,就执行前一个的loader,如果没有就执行下一个loader
if (returnArgs && returnArgs.length && returnArgs.some(arg => arg)) {
context.loaderIndex--;
iterateNomoreLoaders(options, context, returnArgs, pitchingCallback)
} else {
return iteratePitchingLoaders(options, context, pitchingCallback);
}
})
};
let processOptions = {
resourceBuffer: null,
readResource
};
// 调用执行pitch方法
iteratePitchingLoaders(processOptions, loaderContext, (err, result) => {
callback(err, {
result,
resourceBuffer: processOptions.resourceBuffer
})
})
}
module.exports = {
runLoaders
};