编程随想 · 2022 年 02 月 16 日 0

基于 Web Workers 的 SimpleRPC

Web Workers 为前端提供了多线程执行代码的可能,在 worker 内做 CPU 密集型任务再合适不过了。

worker 中的代码在隔离的环境执行,无法访问、操作 DOM,避免了资源竞争,与宿主文档的通信走消息通道

消息机制的优势是解耦,但不适用于拿消息结果的场景,比如:

A -> B
A -> C
A -> D

上例中 A 连续向 B、C、D 各发送了一条消息,B、C、D 处理完任务后再给 A 以回应,在 A 看来,收到的消息可能是这样:

X -> A
X -> A
X -> A

A 并不知道哪个 X 对应 B 的响应,同理,C、D 也无法对号入座。

如何解决呢?一种解决方式是为消息带上目的标识,worker 往回发消息时同样带上自身标识。

做完各种边界情况的处理后,就是通用的远程调用(简易)解决方案了:RPC

定义类型:

interface WorkerMessage {
  id: number;
}

export interface WorkerCallMessage extends WorkerMessage {
  method: string;
  args: any[];
}

export interface WorkerReturnMessage extends WorkerMessage {
  value?: any;
  error?: any;
}

client

import { WorkerReturnMessage } from './type';

const worker = new Worker(new URL('./worker', import.meta.url));


class SimpleRPCClient {

    static __instance: SimpleRPCClient;

    static getInstance() {
        if (!this.__instance) {
            this.__instance = new SimpleRPCClient();
        }
        return this.__instance;
    }

    private messageId = 0;

    private listeners: { [key: string]: {
        resolve: (val: any) => void;
        reject: (reason: any) => void;
    } } = {};

    private constructor() {
        worker.addEventListener('message', this.onMessage);
    }

    private onMessage = (event: MessageEvent<WorkerReturnMessage>) => {
        const { id, value, error } = event.data;
        const { resolve, reject } = this.listeners[id];
        if (error && reject) {
          reject(error);
        } else if (value && resolve) {
          resolve(value);
        }
        delete this.listeners[id];
    }

    call<A extends any[] = any[], R = any>(method: string, ...args: A): Promise<R> {
        return new Promise((resolve, reject) => {
            const id = this.messageId++;
            this.listeners[id] = { resolve, reject };
            worker.postMessage({ id, method, args });
        });
    }
}

worker

import { WorkerCallMessage } from './type';

self.addEventListener('message', (event: MessageEvent<WorkerCallMessage>) => {
  const { id, method, args } = event.data;
 if (method === 'xxx') {
     self.postMessage({
         id,
         value: 'xxxx'
     })
 }
});