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

841 阅读6分钟

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

码字不易,感谢阅读,你的点赞让人振奋!
如文章有误请移步评论区告知,万分感谢!
别说”不喜勿喷“,喷人者我必喷之!
未经授权不得转载!

一、前情回顾

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

  1. 用于控制并发创建的 queue 的 worker 函数的代码结构和大致功能:调用 loaderRunner.runLoaders;
  2. 另外还讨论了 runLoaders 方法,接收 options 和 callback(loader结果函数);
  3. 期间还讨论了 loaderContext 作用,还对比了 worker.js 的 loaderContext 对象和 webpack 的 loaderContext 对象;
  4. 借助两个 loaderContext 回顾了进程间通信,还铺垫了 worker.js 中实现的 loaderContext 上的方法的统一规律;

本篇小作文开始详细讨论一下 worker.js 中构造的 loaderContext 上的各个方法的实现,本文以 loadModule 为例,详细讲述被子进程运行的 loader 如何完成 loadModule 工作的全过程。

二、loaderContext 对象

上一篇小作文已经详述了 loaderContext 对象的作用以及 webpack loaderContext 和 worker.js 的 loaderContext 的联系和区别,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
    },
}

三、loaderContext.loadModule

webpack 的 loaderContext.loadeModule 的原意为解析指定的 request 到一个模块,并对其应用已经配置的 loader,最后向其接收到的 callback 中传入 source、sourceMap、Module实例。

接着我们看看在 worker.js 中这个 loadModule 实现:

context: {
  version: 2,
  fs,
  loadModule: (request, callback) => {
    callbackMap[nextQuestionId] = (error, result) =>
      callback(error, ...result);
      
    writeJson({
      // 这个对象就是发送给父进程的 message
      
      type: 'loadModule',
      id,
      questionId: nextQuestionId, // questionId 和 callback 对应
      request, // 要加载的 request
    });
    nextQuestionId += 1;
  },
}

3.1 callbackMap 缓存 callback

向 callbackMap 中增加一项:key 是 nextQuestionId 这个查询 id,value 是一个函数,这个函数会调用 loadModule 收到的回调,这个回调也就是上面接收 source、sourceMap、Module 实例的 callback,这里相当于建立 nextQuestionId 和 callback 的一个对应关系;

3.2 把工作委给托父进程

通过 writeJson 方法向子进程的 writePipe 写入一个任务,这个任务也是子进程委托父进程去做的事情。调用这个方法后父进程的 readPipe 就可以读取到这个消息,读取到消息后出发 onWorkerMessage 方法,根据 type 处理;

3.3 父进程的 onWorkerMessage 的实现:

  1. 从 message 中得到 request、questionId ;
  2. 通过 id 获取到调用 thread-loader.pitch -> workerPool.run 时传入的 data 对象,这 data 中有 webpack loaderContext.loadModule 的引用;
  3. 调用上一步得到的 data 对象上的 loadModule 方法传入 request 和接收结果的回调函数,在回调函数中通过管道向子进程写入结果,这个操作会触发子进程的 onMessage 方法,如此一来父进程就完成了子进程托付的工作;
// class PoolWorker 的原型方法
onWorkerMessage(message, finalCallback) {
  const { type, id } = message; // message 子进程传递过来的消息
  switch (type) {
    case 'job': {
       // type job ...
       break;
    }
    
    // 处理子进程 type:loadModule
    case 'loadModule': {
      // 从 message 中获取  request 和 questionId
      const { request, questionId } = message;
      
      // 利用 id 取出 data 对象
      const { data } = this.jobs[id];
      
      // 这个 data 就是 tread-loader.pitch 传入的 data 对象
      // 这个 data 对象中的 loadModule 是  webpack loaderContext.loadModule
      data.loadModule(request, (error, source, sourceMap, module) => {
        // 这个回调函数就是调用 webpack loaderContext.loadModule 
        // 之后运行的结果回调
        
        
        // 这里是重点,拿到结果之后通过管道向把结果传递给子进程
        this.writeJson({ // 这个对象就是发送给子进程的 message
          type: 'result',
          id: questionId,
          error: error
            ? {
                message: error.message,
                details: error.details,
                missing: error.missing,
              }
            : null,
          result: [
            source,
            sourceMap,
            // TODO: Serialize module?
            // module,
          ],
        });
      });
      finalCallback();
      break;
    }
 }

3.4 data 对象

data 对象是 thread-loader.pitch 方法中调用 workerPool.run 时传入的,大致如下:

// thread-loader.pitch 方法
function pitch() {

  workerPool.run(
    { // 这个对象就是 data 对象了
      loaders: this.loaders.slice(this.loaderIndex + 1).map((l) => {/*...*/}),
    
      // loadModule,this 是 loaderContext
      loadModule: this.loadModule, 
      
      //....
      optionsContext: this.rootContext || this.options.context,
      rootContext: this.rootContext
    },
    (err, r) => {
      // ...
    }
  );
}

3.5 worker.js 中 onMessage

前面的 3.3 的最后讲到,当父进程调用 data.loadModule 完成工作后会在其回调中调用 this.writeJson({ type: 'result' }) 把结果发送给子进程。

子进程就会读取到父进程发送来消息进而触发 onMessage 方法,接着我们看看 onMessage 的处理过程:

  1. 从 message 中获取 type 和 id,处理 type 为 result 的 case;
  2. 通过 id 从 callbackMap 中取出该 id 对应的回调函数,接着执行它,并传入父进程送来的结果数据;
  3. 从 callbackMap 中移除该 id 对应的记录;
function onMessage(message) {
  try {
    const { type, id } = message;
    switch (type) {
      
      // 处理 type 为 result 的消息
      case 'result': {
        const { error, result } = message;
        const callback = callbackMap[id];
        if (callback) {
          const nativeError = toNativeError(error);
          
          // 这个 callback 是前面 3.1 callbackMap 缓存的回调
          callback(nativeError, result);
        } else {
        
        }
        
        // 移除 id 对应的记录
        delete callbackMap[id];
        break;
      }
     
    }
  } catch (e) {
    
  }
}

3.6 来个例子

  1. 假如我们有个 a-loader 正在被 thread-loader 通过子进程运行,a-loader 的代码如下:
// a-loader
module.exports = function (data) {
  // 这个 this 是 worker.js 中提供的 loaderContext
  this.loadModule('./some/request.js', function aLoaderCb (source, sourceMap, module) {
    // 这个回调将来会被加入到 callbacMap 中
  })
  
  // nothing processed
  retrurn 'some-result-buffer-or-text'
}
  1. 当 thread-loader 的 worker.js 调用 runLoaders() 时就会运行上面的 a-loader,此时就会执行 3.2 中把加载 ./some/request.js 模块的工作委托给父进程——定义 type:loadModule 的消息。同时把上面的 aLoaderCb 缓存到 callbackMap 中,假设 id 是 100,此时 callbackMap[100] = aLoaderCb;

  2. 父进程收到id 是 100 的加载 ./some/request.js 消息后,触发 onWorkerMessage 方法,接着调用 data.loadModule 传入 ./some/request.js 及接收结果的回调;

  3. 当 data.loadModule 加载 ./some/request.js 后调用回调时,回调定义 type:result 的消息, id 为 100 和加载的结果发给子进程;

  4. 当子进程收到type 为 result 的消息后,得到 id 为 100,接着从 callbackMap[100] 对应的 aLoaderCb 拿出来,把加载的结果传递给 aLoaderCb;

四、总结

本篇小作文以 loadModule 为例,详细讲述了被子进程运行的 loader 如何在进程间无法共享内存的背景下调用到处于父进程中的 loaderContext 上的方法,大致过程如下:

  1. 被 worker.js 运行的 loader 调用 this.loadModule,即调用 worker.js loaderContext.loadModule;
  2. worker.js loaderContext.loadModule 通过 id 缓存真正需要结果的 callback 到 callbackMap;
  3. 子进程发送 type:loadModule 的消息委托父进程处理 loadModule 的工作;
  4. 父进程完成 loadModule 的工作后向子进程发送 type:result 的消息并附带加载结果;
  5. 子进程收到 type:result 的消息后从 callbackMap 中取出 callback 调用并传入从父进程发来的结果;

loaderContext 上还有不少基于这种原理的方法,后面将会只讨论这些方法的代码实现而不会再交代整个通信过程,如果还有疑惑欢迎评论区告知啊~