你知道为什么Loader从右向左执行吗?

541 阅读5分钟

「这是我参与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

总结流程为

  1. 执行loader-runner,获取所有loaders,并且转换成绝对路径,合并其他参数传入
  2. 执行runLoaders方法,传入合并后的参数和回调
  1. 开始执行iteratePitchingLoaders,从左向右执行,如果遇到返回值就终止后面的loader,直接执行前一个loader函数
  2. 所有pitch执行完成之后,读取文件资源,然后从右向左的开始执行loader函数
  1. 开始执行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
};

7. 流程图