# 本文介绍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 默认是一个空对象{},normalLoaderpitchLoader 通过 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 loaderundefined 会发生熔断。理解为掉头输出了。

# loader 源码解析

image-20220509155842003 创建步骤 如上图创建对应文件

  • 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。结果如下:

image-20220509160441807

  • 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