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

1,244 阅读5分钟

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

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

一、前情回顾

上一篇篇小作文以 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 上还有不少基于这种原理的方法,下面将会只讨论这些方法的代码实现而不会再交代整个通信过程,如果你是第一次读到这个系列的第一篇,建议你读一读前面的文章呀~

本文接上文接着说 loaderContext 上除了 loadModule 剩下的方法,这些方法只讨论方法实现,不再讨论通信过程,通信过程在 多进程打包:thread-loader 源码(14) 详细讲述过。

二、loaderContext

这里的 loaderContext 是指 thread-loader 的 worker.js 构造的新的 loaderContext,并非 webpack 创造出来的 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) => {},
      
      // ....
    },
}

三、loaderContext.resolve

webpack loaderContext 中 resolve 方法的作用是向 require 方法一样解析一个 request;

在 worker.js 的 loaderContext.resolve 中,它调用了 resolveWithOptions 方法处理这件事:

context: {
  version: 2,
  fs,
  //...
  
  // ...
  resolve: (context, request, callback) => {
    // 调用 resolveWithOptions 方法
    resolveWithOptions(context, request, callback);
  },
}

3.1 resolveWithOptions 方法

这个方法是 worker 函数的私有方法,它的实现核心同 loadModule 一样,把真正需要 resolve 结果的 callback 通过 nextQuestionId 缓存到 callbackMap,接着向父进程发送 type: resolve 的消息,委托父进程去解析这个 request;

const resolveWithOptions = (context, request, callback, options) => {

  // 缓存 callback
  callbackMap[nextQuestionId] = callback;
  
  // 发送 type:resolve 的消息委托父进程解析request
  writeJson({
    type: 'resolve',
    id,
    questionId: nextQuestionId,
    context,
    request,
    options,
  });
  nextQuestionId += 1;
};

四、loaderContext.getResolve

在 webpack 的 loaderContext.getResolve 这个方法用于创建一个类似上面 resolve 的函数,它接收配置选项,这些配置自动会和 webpack 配置的 resolve 选项合并,它接收可以接收回调,也可以返回一个 Promise 对象;这个方法在中文文档上没有,只能贴英文的了;

在 worker.js 的 getResolve 方法中,getResolve 是用上文的 resolveWithOptions 方法实现的,不再赘述;

context: {
  version: 2,
  fs,
  // ...
  // 注意 getResolve 是返回一个函数!!函数!!!
  getResolve: (options) => (context, request, callback) => {
    if (callback) {
      // 回调形式
      resolveWithOptions(context, request, callback, options);
    } else {
      // Promise 形式
      return new Promise((resolve, reject) => {
        resolveWithOptions(
          context,
          request,
          (err, result) => {
            if (err) {
              reject(err);
            } else {
              resolve(result);
            }
          },
          options
        );
      });
    }
  },
}

五、loaderContext.getOptions

在 webpack loaderContext.getOptions 中,该方法用于提取给定的 loader 的选项,它接收一个可选的 json-schema 对象,用于校验选项;

在 worker.js 中的 getOptions 实现如下:

  1. 使用 loaderIndex 从 this.loaders 中取到当前的 loader,loaderIndex 是一个索引,是一个 loader-runner 这个库维护的,用于运行 loaders 时从 loaders 数组中取用 loader,pitch 阶段递增,normal 阶段递减;

  2. 获取 loader 上的 options 属性,针对 options 的类型进行抹平,最终变成对象;

  3. 对 options 进行非空的保证工作;

  4. 如果传递了 schema,就对调用 validate 进行校验;

  5. 最后返回 options

context: {
  version: 2,
  fs,
  // ...
  // getOptions 使用了 this,所以不用箭头函数
  getOptions(schema) {
    // 取出 loader
    const loader = this.loaders[this.loaderIndex];

    // 获取 options
    let { options } = loader;
    
    // 类型抹平
    if (typeof options === 'string') {
      if (options.substr(0, 1) === '{' && options.substr(-1) === '}') {
        try {
          options = parseJson(options);
        } catch (e) {
          throw new Error(`Cannot parse string options: ${e.message}`);
        }
      } else {
        options = querystring.parse(options, '&', '=', {
          maxKeys: 0,
        });
      }
    }

    // 确保 options 非空
    if (options === null || options === undefined) {
      options = {};
    }
    
    // 传了 schema 的话进行校验
    if (schema) {
      let name = 'Loader';
      let baseDataPath = 'options';
      let match;
      // eslint-disable-next-line no-cond-assign
      if (schema.title && (match = /^(.+) (.+)$/.exec(schema.title))) {
        [, name, baseDataPath] = match;
      }
      validate(schema, options, {
        name,
        baseDataPath,
      });
    }

    return options;
  },

六、loaderContext.emitWarning/emitError

用于向 webpack 编译后输出的的警告、错误中写入 loader 运行过程中遇到的警告和错误;

这两个方法很简单,利用管道将警告和错误信息发送到父进程,让父进程调用 webpack loaderContext 上的 emitError/emitWarning 进行处理;

emitWarning: (warning) => {
  writeJson({
    type: 'emitWarning',
    id,
    data: toErrorObj(warning),
  });
},
emitError: (error) => {
  writeJson({
    type: 'emitError',
    id,
    data: toErrorObj(error),
  });
},

七、loaderContext.exec

webpack loaderContext.exec 方法 用于向模块一样执行一些代码片段,这方法在 webpack5 的文档上没有找到,估计被移除了。。。

worker.js 的 exec 方法其实是一种 github 上的实现:通过原生 module 模块加载编译并执行这个代码片段返回结果,相当在运行时动态创建了一个 CommonJS 的模块;

context: {
  version: 2,
  fs,
  // ...
  
  // exec 的实现
  exec: (code, filename) => {
    const module = new NativeModule(filename, this);
    module.paths = NativeModule._nodeModulePaths(this.context);
    module.filename = filename;
    module._compile(code, filename); 
    return module.exports;
  },
  
}

八、loaderContext.addBuildDependency

目前没有从 webpack 文档的 loaderContext 搜到 addBuildDependency 方法。

但是这里的这个实现很简单就是把接收到 filename 加入到 buildDependencies 数组中

context: {
  version: 2,
  fs,
  // ...
 
  addBuildDependency: (filename) => {
    buildDependencies.push(filename);
  },
}

九、总结

本文把 worker.js 实现的 loaderContext 上的各个方法挨个分析了一遍,这里并不包含父子进程通信的部分,如果对这部分还有疑惑请移步上一篇文章哈,本文主要分析了以下方法:

  1. loaderContext.resolve 解析一个 request;
  2. loaderContext.getResolve 方法,获取一个解析 request 的函数,可以支持 callback 和 Promise;
  3. loaderContext.getOptions 获取 loader 的选项对象;
  4. loaderContext.emitWarning/emitError 抛出警告和错误信息,编译后输出到命令行;
  5. loaderContext.exec 执行代码片段;
  6. loaderContext.addBuildDependency 方法,收集 buildDependency;

到这里,runLoader 的第一个参数基本已经结束了,下文我们准备讨论 runLoaders 的结果回调,即 loader 运行结束之后的逻辑