webpack之loader运行机制及简单实现

411 阅读5分钟

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执行机制示意图

    1. 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
    1. 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 内部实现逻辑

    1. 首先要判断当前loader的loaderIndex是否大于所有需要执行loader的长度, 如果超出则需要调用processSource去读当前要处理的文件源码, 源码读取完会执行iterateNormalLoader调用normal-loader
    2. 取出当前的loader对象并获取pitch-loader函数, 如果loader定义了pitch-loader函数,并且pitch-loader有返回值, 那就直接调用上一个normal-loader, 否则 loaderIndex+1 执行iteratePitchingLoaders查找下一个
    3. 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 内部实现逻辑
    1. 如果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);
       }
   }
   

代码实现地址 github.com/liyanjunCod… webpack文件夹中loaders 的runLoader.js文件