像进行网络请求一样使用 Web Worker
前言
Web Worer
使得浏览器可以在 Worker
线程中运行脚本,而不会阻塞主线程。
Worker
线程中处理的数据大多数情况下最终是需要传到主线程中使用的。Worker
线程中的脚本和主线程的脚本之间通过 postMessage()
方法发送消息,通过监听 message
事件接受消息。如果一个 Worker
脚本执行一个单一的任务,那么主线程和 Worker
线程之间可以很方便地互相发送一次消息便能完成这个 Worker
脚本的任务。但是如果一个 Worker
脚本需要与主线程脚本频繁交互,甚至如果有一些第三方库需要在 Worker
线程中执行,而它又有很多 API
需要在主线程中决定如何去调用,这样的场景中怎样才能较好地管理 Worker
线程和主线程之间的消息传递呢?
worker-handler
worker-handler 旨在组织上述场景中两个线程之间的消息传递。
worker-handler
通过在 Worker
脚本中定义一系列 Action
函数来规定主线程可以要求 Worker
执行哪些操作。之后,在主线程中可以像进行网络请求一样向 Worker
发送请求和接收响应。有两种方式获得消息响应,可以通过 Promise
获取,就像 AJAX
,一个请求对应一个响应,也可以通过 EventTarget
获取,就像 Server-sent events
,一个请求可以收到多个响应,并且这两种响应方式可以在同一个请求中同时使用。
快速开始
以下是一个 worker-handler
最简的用法示例:
npm install worker-handler
// demo.worker.js
import { createOnmessage } from "worker-handler/worker";
// 传入 Actions 调用 createOnmessage 以创建 worker 的 onmessage 回调
onmessage = createOnmessage({
// 如果只使用 Promise 响应方式,推荐使用 async 函数定义 Action
async someAction() {
// Action 中可以执行任意异步内容
......
// 异步 Action 中返回的内容将作为响应内容以 promise 的形式传递给 Main
return "some messages";
}
});
// demo.main.js
import { WorkerHandler } from "worker-handler"; // 也可以从 "worker-handler/main" 中引入
// import workerUrl from "./demo.worker.ts?worker&url"; // in vite
// import workerInstance from "./demo.worker.ts?worker"; // in vite
const demoWorker = new WorkerHandler(
// 如果是在 vite 环境中,可以传入上面的 workerUrl 或 workerInstance
new Worker(new URL("./demo.worker.js", import.meta.url)) // webpack5 环境中以这种方式创建 Worker 实例
);
// 请求 Worker 执行 someAction
demoWorker.execute("someAction", []).promise.then((res) => {
// 接收 Action 中以 Promise 形式响应的内容
console.log(res.data);
}).catch((err) => {
// Action 中发生的错误会使得 promise 被 reject
console.log(err)
});
类型支持
在 typescript
中使用 worker-handler
时,定义好 Actions
的类型后,便可以在传递消息的发送端和接收端都能进行类型检测和提示,并且可以检测传递的消息是否可以被结构化克隆算法处理,是否需要处理可转移对象等。
以下是 typescript
中使用 worker-handler
的简单示例:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
/*
* 定义 Actions 的类型,之后有以下两处地方需要将其作为泛型参数传入:
* - 在 Worker 中使用 createOnmessage() 时
* - 在 Main 中使用 new WorkerHandler() 时
*/
export type DemoActions = {
// 定义一个名为 pingLater 的 Action,其返回值类型 ActionResult<string> 表示该 Action 可以传递给 Main 的消息类型为 string
pingLater: (delay: number) => ActionResult<string>;
};
onmessage = createOnmessage<DemoActions>({
// pingLater 执行后会在 delay 毫秒后将消息传递给 Main
async pingLater(delay) {
await new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, delay);
});
return "Worker recieved a message from Main " + delay + "ms ago.";
}
});
// demo.main.ts
import { WorkerHandler } from "worker-handler/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
demoWorker.execute("pingLater", [], 1000).promise.then((res) => {
console.log(res.data);
});
响应消息
Promise
形式
有没有觉得上面的 pingLater
中想要实现指定特定延迟后再发送消息还需要自己 new
一个 Promise
很麻烦?这当然不合理,但是别担心,Promise
形式的消息传递除了使用 Action
的返回值,还支持通过调用 Action
中的 this.$end()
方法,可以在回调函数中使用,且同样支持类型检测和提示。所以上面的示例更适合这么写:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler-test/worker";
export type DemoActions = {
// 这里返回值类型被定义为 ActionResult<string | void>,表示传递的消息类型应为 string,并且该异步函数可能不会显式地返回一个值
pingLater: (delay: number) => ActionResult<string | void>;
};
onmessage = createOnmessage<DemoActions>({
async pingLater(delay) {
setTimeout(() => {
this.$end("Worker recieved a message from Main " + delay + "ms ago.");
}, delay);
}
});
this.$end()
方式适合 Action
在发出响应后仍需要继续执行的情况,或需要在 Action
中的回调函数中发出响应的情况,但是要注意,它无法在使用箭头函数定义的 Action
中使用。而函数返回值的方式适合当 Action
中所有逻辑执行完毕后再做出响应的情况,且可以在箭头函数中使用。
EventTarget
形式
如果需要一个请求对应多次响应,那么就需要使用 EventTarget
形式的响应。在 Action
中调用 this.$post()
可以将消息以 EventTarget
形式传递给主线程,同样, Action
不能被定义为箭头函数。
下面是一个同时使用 EventTarget
和 Promise
形式响应消息,并在主线程中接收它们的示例:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler-test/worker";
export type DemoActions = {
// EventTarget 形式传递的消息类型也通过 Action 的返回值类型定义,ActionResult<string | void> 表示传递的消息类型是 string,并且该异步函数可能不会显式地返回一个值
pingInterval: (
interval: number,
isImmediate: boolean,
duration: number
) => ActionResult<string | void>;
};
// 调用 pingInterval() 后,每隔 interval 毫秒就会发送一次 EventTarget 形式的消息,在 duration 毫秒后会发送 Promise 形式的消息并关闭请求连接
onmessage = createOnmessage<DemoActions>({
async pingInterval(interval, isImmediate, duration) {
let counter = 0;
const genMsg = () => "ping " + ++counter;
if (isImmediate) this.$post(genMsg());
const intervalId = setInterval(() => {
this.$post(genMsg());
}, interval);
setTimeout(() => {
clearInterval(intervalId);
this.$end("no longer ping");
}, duration);
}
});
// demo.main.ts
import { WorkerHandler } from "worker-handler/main";
import { DemoActions } from "./demo.worker";
const demoWorker = new WorkerHandler<DemoActions>(
new Worker(new URL("./demo.worker.ts", import.meta.url))
);
demoWorker
.execute("pingInterval", [], 1000, false, 5000) // execute() 执行后会返回一个 MessageSource
.addEventListener("message", (e) => {
console.log(e.data);
}) // 如果使用 addEventListener() 的方式监听 MessageSource,则会将 MessageSource 本身再次返回,使得可以链式调用
.promise.then((res) => {
console.log(res.data);
});
执行 demo.main.ts
后,会在控制台输出如下内容:
调用 Action
在 Main
中执行 WorkerHandle
实例的 excute()
会与 Worker
产生一个连接,并执行一个 Action
。
excute()
接收的第三个以后的参数会按顺序传递给 Worker
中对应的 Action
。
第二个参数可以接收一个连接配置选项对象,包含 transfer
和 timeout
两个属性:
transfer
是一个会被转移所有权到Worker
中的的可转移对象数组。timeout
是本次连接的超时时间。超时后该连接将会被关闭,不会再收到任何响应,且Action
返回的Promise
将转变为rejected
状态。
也可以简化传参:
- 如果只需要使用
transfer
,可以直接传入一个数组。 - 如果只需要使用
timeout
,可以直接传入一个数字。 - 如果都不需要开启,那么可以传入以下任意值:
null
、undefined
、[]
、小于或等于0
的任何数字。
Transfer
如果传递的消息中包含可转移对象,那么需要对其进行所有权进行转移处理。
主线程传递给 Worker
时,通过 workerHandle.execute()
的第二个参数进行指定。
Worker
传递给主线程时,Action
中 this.$end()
和 this.$post()
的第二个参数都是用来指定 transfer
数组的。
在使用 Action
的返回值进行响应时,返回值如果是一个数组,那么该数组只能有两个项,第一项是传递的消息,第二项就是指定的 transfer
数组。这也导致,如果 Action
返回值中如果希望传递数组类型的消息,必须通过 [messageData, [...transferable]]
的形式,即使不需要处理 transfer
,例如:
// demo.worker.ts
import { ActionResult, createOnmessage } from "worker-handler/worker";
export type DemoActions = {
getRandomNumsInArray: (amount: number) => ActionResult<number[]>;
};
onmessage = createOnmessage<DemoActions>({
async getRandomNumsInArray(amount) {
const numsArr = [];
for (let i = 0; i < amount; i++) {
numsArr.push(Math.round(Math.random() * 100));
}
// 如果这里是 "return numsArr",则 TS 类型检测不会通过
return [numsArr, []];
},
});
不过也不用担心不小心传错,当在 TS
中使用时,不符合要求的传参都会在写代码时就出现提示。
结语
以上基本涵盖了 worker-handler
的使用方式,具体的 API
可以在这里查看。
源码在这里,感兴趣的话给个 star
呗~
转载自:https://juejin.cn/post/7366516215378362418