持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的19天,点击查看活动详情
码字不易,感谢阅读,你的点赞让人振奋!
如文章有误请移步评论区告知,万分感谢!
别说”不喜勿喷“,喷人者我必喷之!
未经授权不得转载!
一、前情回顾
上一篇小作文讨论了一下 worker.js 中以下功能:
- 用于控制并发创建的 queue 的 worker 函数的代码结构和大致功能:调用 loaderRunner.runLoaders;
- 另外还讨论了 runLoaders 方法,接收 options 和 callback(loader结果函数);
- 期间还讨论了 loaderContext 作用,还对比了 worker.js 的 loaderContext 对象和 webpack 的 loaderContext 对象;
- 借助两个 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 的实现:
- 从 message 中得到 request、questionId ;
- 通过 id 获取到调用 thread-loader.pitch -> workerPool.run 时传入的 data 对象,这 data 中有 webpack loaderContext.loadModule 的引用;
- 调用上一步得到的 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 的处理过程:
- 从 message 中获取 type 和 id,处理 type 为 result 的 case;
- 通过 id 从 callbackMap 中取出该 id 对应的回调函数,接着执行它,并传入父进程送来的结果数据;
- 从 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 来个例子
- 假如我们有个 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'
}
-
当 thread-loader 的 worker.js 调用 runLoaders() 时就会运行上面的 a-loader,此时就会执行 3.2 中把加载 ./some/request.js 模块的工作委托给父进程——定义 type:loadModule 的消息。同时把上面的 aLoaderCb 缓存到 callbackMap 中,假设 id 是 100,此时 callbackMap[100] = aLoaderCb;
-
父进程收到id 是 100 的加载 ./some/request.js 消息后,触发 onWorkerMessage 方法,接着调用 data.loadModule 传入 ./some/request.js 及接收结果的回调;
-
当 data.loadModule 加载 ./some/request.js 后调用回调时,回调定义 type:result 的消息, id 为 100 和加载的结果发给子进程;
-
当子进程收到type 为 result 的消息后,得到 id 为 100,接着从 callbackMap[100] 对应的 aLoaderCb 拿出来,把加载的结果传递给 aLoaderCb;
四、总结
本篇小作文以 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 上还有不少基于这种原理的方法,后面将会只讨论这些方法的代码实现而不会再交代整个通信过程,如果还有疑惑欢迎评论区告知啊~