持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的20天,点击查看活动详情
码字不易,感谢阅读,你的点赞让人振奋!
如文章有误请移步评论区告知,万分感谢!
未经授权不得转载!!!!!!!!!!!
一、前情回顾
上一篇篇小作文以 loadModule 为例,详细讲述了被子进程运行的 loader 如何在进程间无法共享内存的背景下调用到处于父进程中的 loaderContext 上的方法,大致过程如下:
- 被 worker.js 运行的 loader 调用 this.loadModule,即调用 worker.js loaderContext.loadModule;
- worker.js loaderContext.loadModule 通过 id 缓存真正需要结果的 callback 到 callbackMap;
- 子进程发送 type:loadModule 的消息委托父进程处理 loadModule 的工作;
- 父进程完成 loadModule 的工作后向子进程发送 type:result 的消息并附带加载结果;
- 子进程收到 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 实现如下:
-
使用 loaderIndex 从 this.loaders 中取到当前的 loader,loaderIndex 是一个索引,是一个 loader-runner 这个库维护的,用于运行 loaders 时从 loaders 数组中取用 loader,pitch 阶段递增,normal 阶段递减;
-
获取 loader 上的 options 属性,针对 options 的类型进行抹平,最终变成对象;
-
对 options 进行非空的保证工作;
-
如果传递了 schema,就对调用 validate 进行校验;
-
最后返回 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 上的各个方法挨个分析了一遍,这里并不包含父子进程通信的部分,如果对这部分还有疑惑请移步上一篇文章哈,本文主要分析了以下方法:
- loaderContext.resolve 解析一个 request;
- loaderContext.getResolve 方法,获取一个解析 request 的函数,可以支持 callback 和 Promise;
- loaderContext.getOptions 获取 loader 的选项对象;
- loaderContext.emitWarning/emitError 抛出警告和错误信息,编译后输出到命令行;
- loaderContext.exec 执行代码片段;
- loaderContext.addBuildDependency 方法,收集 buildDependency;
到这里,runLoader 的第一个参数基本已经结束了,下文我们准备讨论 runLoaders 的结果回调,即 loader 运行结束之后的逻辑