多进程打包:thread-loader 源码(13)

1,434 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的18天,点击查看活动详情

码字不易,感谢阅读,你的点赞让人振奋!
如文章有误请移步评论区告知,万分感谢!
未经授权不得转载!

一、前情回顾

上文详细讨论了 onMessage 的代码结构和代码的意义,另外讨论了其中的细节问题如下:

  1. onMessage 分为三种类型的消息:

    • 1.1 代表任务的 job,任务 pushqueue
    • 1.2 代表结果的 result,接收父进程的返回结果
    • 1.3 代表预热的 warmup,把需要的模块加载到子进程节约时间;
  2. 创建 queue 时传入的 worker 函数负责跑 loaderworker 函数细节并未展开讨论;

  3. typeresult 是进程间通信的一环,子进程中的 loaders 需要了某些方法,碍于进程通信无法传递方法,所以委托父进程去调用,再通过回调把结果给到子进程;

本篇小作文正式讲解跑 loaderworker 函数的具体实现细节!

二、asyncQueue 的 worker 函数

asyncQueue 来自 neo-async/queue.js,老相识了,后面要说的是它的第一个参数 worker 函数,也是 thread-loader 中最终运行 loader 的函数了。

2.1 回顾 neo-async/queue.js

在讨论父进程代码中 WorkerPool 的时候详细讲述过 neo-async/queue 的工作原理,他接收一个 worker 函数和一个表示并发数目的数字,返回一个队列 queue

当有数据被 pushqueue 时,neo-async 会调用 runQueue 方法消耗队列,runQueue 内部会调用创建 queue 时传入 worker 函数处理 data,并且给 worker 函数传入一个 done 方法,这个 done 将会在 worker 函数执行到结束时调用,意在告知 queue 本次 worker 函数执行已经结束。

在调用 runQueue 的过程中需要判断当前已经在运行的任务是否超出创建 queue 时传入的并发数限制,如果超过了就会暂停。

const queue = asyncQueue(({ id, data }, taskCallback) => {
  // 这个箭头函数就是 worker 函数了
  // taskCallback 就是 runQueue 的 done 方法
}, PARALLEL_JOBS);

关于 neo-async/queue 暂时就说这么多,本文的重点是 worker 函数;

2.2 worker 函数代码结构

2.2.1 参数:

  1. { id, data },这个位置的参数就是前面 pushqueue 里面的 data
  2. taskCallbackrunQueue 里面传入的 done

2.2.2 方法内部逻辑:

  1. 创建 resolveWithOptions 方法,这个方法是下面 runLoadersloaderContext.resolve 方法的实现;
  2. 声明常量 buildDependencies 数组
  3. 调用 loaderRunner.runLoaders 方法,传入 runLoaders 所需参数(包含 loaderContext 对象)、接收 loader 结果的回调函数,这些参数后面会详细讨论;
const queue = asyncQueue(({ id, data }, taskCallback) => {
  // taskCallback 就是 runQueue 的 done
  try {
    // loaderContext.resolve 方法的实现
    const resolveWithOptions = (context, request, callback, options) => {};

    // 保存本次 runLoaders 得到的 buildDependencies
    const buildDependencies = [];
    
    // 调用 loaderRunner.runLoaders 跑 loaders
    loaderRunner.runLoaders(
      {
        // runLoaders 所需的选项对象,包含一个模拟出来的 loaderContext
        loaders: data.loaders,
        context: { /* 模拟出来的 loaderContext */ }
      },
      (err, lrResult) => {
         // 处理 loader 运行结束后的结果
         // 这个函数给他取个名字,后面叫他 loader 结果回调
      }
     );
  } catch (e) {
  
    taskCallback();
  }
}, PARALLEL_JOBS);

三、 runLoader 方法

  1. 方法位置:node_modules/loader-runner/lib/LoaderRunner.js

  2. 方法参数:

    • 2.1 options: 选项对象,这里面包含要运行的 loaderloaderContext 对象,这个 options 对象是我们下午要讨论的一个 重点;
    • 2.2 callback: 当 loader 运行结束后要执行的回调函数,也就是我们上面说的 loader 结果回调
  3. 方法作用:这里并不会具体讨论 runLoaders 的全部工作,为了便于让大家理解这个选项对象的作用才临时加入了这个片段。推荐另一篇详细介绍 webpack 运行 loader 过程的文章:包看包会: webpack run loader

    • 3.1 从 options 上获取传入的 context 属性,即 loaderContext,对其进行扩展,包括 addDependenciesasync、等方法都是在这个时间点扩展的;
    • 3.2 调用 iteratePitchingLoaders 方法并传入经过扩展的 loaderContext 对象。这个 iteratePitchingLoaders 方法加重 loader 模块,并执行 loaderpitch 方法,这个阶段也就是 pitch 阶段。当 pitch 阶段运行结束后自动进入 normal 阶段,在 normal 结束后调用 callback 并把 loader 运行的结果传递给 callback
exports.runLoaders = function runLoaders(options, callback) {
   
    var loaderContext = options.context || {};
     
    // 扩展 loaderContext
    loaderContext.dependency = loaderContext.addDependencies = function addDependency () {
    }
    
    // ....
    
    // 调用 iteratePitchingLoaders 加载并运行 loader 的 pitch 方法,
    // 进入 pitch 阶段;
    // pitch 阶段结束后自动进入 normal 阶段,
    // 结束后调用 callback 回调即 loader 结果回调
    iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {
        // 调用 runLoaders 回调传入 result
        callback(null, {
         result: result,
         resourceBuffer: processOptions.resourceBuffer,
         cacheable: requestCacheable,
         fileDependencies: fileDependencies,
         contextDependencies: contextDependencies
        });
    }
}

3.1 webpack 调用 runLoaders

之所以提这个点,也是便于大家理解 worker 做这些工作的初衷,下图是 webpack 内部调用的 runLoaders 方法,图中的 context: loaderContext 就是 webpack 内部初始化的 loaderContext 这个上下文对象,也就是 loader 函数内部 this 所绑定的对象,上文档传送门

image.png

3.2 worker.js 中的 loaderContext vs webpack 的 loaderContext

大家思考一个问题,为什么在这里调用 runLoader 传入的 context 是一个新构造的对象,而不是 webpack 中的 loaderContext 对象(在作用上这两个对象时等价的)?

如果你很快就反应出答案,说明前面的内容你已经滚瓜烂熟了:是因为在 thread-loader 中调用 runLoaders 这个方法是在 worker.js 中调用的,而 worker.js 又是在子进程中的调用。碍于进程间通信的限制,进程间通信使用的自定义管道的方式实现的,而这种实现方式传递的是被序列化的 JSON 字符串。

这就导致 webpack 中的 loaderContext 对象无法被传递到子进程中,究其根本,是因为进程间的内存是隔离的,webpackloaderContext 对象存在于父进程,而 runLaoders 却是在子进程中。所以当子进程需要时,只能再造一个新的对象。

这个新造的对象包含了 loaderContext 应有的属性和方法,但是这些方法并不直接处理工作,而是转发这些工作到父进程让父进程完成,父进程完成后把结果发送给子进程。

这里就揭示了这个全新的 loaderContext 的核心实现,虽然这里没有代码,但是请记住,这个核心:转发工作个父进程,等待接收父进程传送来的结果。

3.3 webpack loaderContext

这里就偷个懒上个截图吧,只需要关注一些方法和属性,后面的 worker.js 会同样实现一份这样的方法和属性出来;

image.png

3.4 worker.js loaderContext

let cfg = {
    loaders: data.loaders, // 要跑的 loader
    resource: data.resource, // 
    readResource: fs.readFile.bind(fs),
    context: { // worker.js 的 loaderContext 对象
      version: 2,
      fs,

      // 模拟 loaderContext 的 loadModule 方法
      loadModule: (request, callback) => {}, 

      // 模拟 loaderContext 的 resolve 方法
      resolve: (context, request, callback) => {}, 

     // 模拟 loaderContext 的 getResolve 方法
      getResolve: (options) => (context, request, callback) => {}, 

      // 模拟 loaderContext 的 getOptions 方法
      getOptions(schema) {},

      // 模拟 loaderContext 的 emitWarning 方法
      emitWarning: (warning) => {},

      // 模拟 loaderContext 的 emitError 方法
      emitError: (error) => {},

      // 模拟 loaderContext 的 exec 方法
      exec: (code, filename) => {},

      // 模拟 loaderContext addBuildDependency 方法
      addBuildDependency: (filename) => {},
      options: {},
      webpack: true,
      'thread-loader': true,
      sourceMap: data.sourceMap,
      target: data.target,
      minimize: data.minimize,
      resourceQuery: data.resourceQuery,
      rootContext: data.rootContext
    },
}

四、总结

本篇小作文讨论了一下 worker.js 中以下功能:

  1. 用于控制并发创建的 queue 的 worker 函数的代码结构和大致功能;
  2. 另外还讨论了 runLoaders 方法,接收 optionscallbackloader结果函数);
  3. 期间还讨论了 loaderContext 作用,还对比了 worker.jsloaderContext 对象和 webpackloaderContext 对象;
  4. 借助两个 loaderContext 回顾了进程间通信,还铺垫了 worker.js 中实现的 loaderContext 上的方法核心;