简介架构Web Worker 架构

Web Worker 架构

一些模块,例如公式引擎,天生是比较消耗资源的,为了避免阻塞主线程从而影响交互体验,Univer 支持将这些模块运行在 Web Worker 当中。

运行在 Web Worker 当中的 Univer 代码实际上也是一个 Univer 实例,两个 Univer 实例之间通过 @univerjs/rpc 包提供的 RPC 机制进行通信,@univerjs/rpc 插件还提供了在两个 Univer 实例之间同步数据和 mutation 的机制。

例子

在 Univer 当中使用 Web Worker 并使用基于 Web Worker 的公式计算非常简单。

在主线程的 Univer 实例中引入 @univerjs/rpc 插件,指定要加载的 Web Worker 入口文件的路径,并将主线程中的公式插件配置为无需计算公式:

import type { IUniverRPCMainThreadConfig } from '@univerjs/rpc';
import { UniverRPCMainThread } from '@univerjs/rpc';
import { UniverSheetsFormula } from '@univerjs/sheets-formula';
 
// ...
 
univer.registerPlugin(UniverSheetsFormula, {
  notExecuteFormula: true,
})
 
univer.registerPlugin(UniverRPCMainThread, {
  workerURL: './worker.js',
  unsyncMutations: new Set([RichTextEditingMutation.id])
});

在 Web Worker 的入口文件中实例化另一个 Univer 实例,并注册需要的插件:

import { UniverFormulaEngine } from '@univerjs/engine-formula';
import { UniverSheets } from '@univerjs/sheets';
import { LocaleType, Univer } from '@univerjs/core';
import { UniverRPCWorkerThreadPlugin } from '@univerjs/rpc';
import { UniverSheetsFormula } from '@univerjs/sheets-formula';
 
const univer = new Univer();
 
univer.registerPlugin(UniverRPCWorkerThreadPlugin);
univer.registerPlugin(UniverSheets);
univer.registerPlugin(UniverFormulaEngine);
univer.registerPlugin(UniverSheetsFormula);

运行之后,就会看到 Web Worker 线程,并且公式计算会在其中进行。

ℹ️

可以看到因为在 Web Worker 中无需关心 UI 和渲染,所以引入的 plugin 比主线程中少很多,这就是插件化架构带来的好处之一:在任何环境中 Univer 都能做到以最小的资源运行。

详细内容请参考 examples/sheets

整体架构

Univer 的 Web Worker 架构图下图所示:

Web Worker 架构

主线程中的创建的文档和发生的编辑会被同步到 Web Worker:

  1. 主线程中的 Univer 实例在文档创建时,位于主线程的 DataSyncPrimaryController 会监听到该事件,然后调用 Web Worker 中的 IRemoteInstanceServicecreateInstance 方法创建一个同样的文档实例。
  2. 主线程中的 Univer 实例在执行 mutation 时,位于主线程的 DataSyncPrimaryController 会监听到该事件,然后调用 Web Worker 中的 IRemoteInstanceServicesyncMutation 方法将该 mutation 应用到 Web Worker 中的 Univer 实例。

Web Worker 的公式计算结果如何写回主线程:

  1. Web Worker 中的 Univer 实例在公式计算完毕时会生成一个 mutation 并引用,位于 Web Worker 的 DataSyncReplicaController 会监听到该事件,然后调用主线程中的 IRemoteSyncServicesyncMutation 方法将该 mutation 应用到主线程中的 Univer 实例。

RPC 机制

从上面的叙述中,可以看到位于不同线程中的 Univer 模块似乎可以直接调用对方的方法,这是因为 @univerjs/rpc 插件提供了 RPC 机制。

以主线程调用 Web Worker 线程中 IRemoteInstanceService 为例。在作为客户端的主线程中,需要声明一个 IRemoteInstanceService 接口作为依赖,而这个依赖的具体实现,则是 RPC 模块提供的 toModule 方法,该方法接受一个 Channel 作为参数,返回一个实现了 IRemoteInstanceService 接口的对象,该对象实际上是一个 Proxy,对该对象的方法的调用会被 Proxy 拦截,channel 名称,方法名称和参数会被 RPC 模块序列化之后发送到 Web Worker 线程。

export class UniverRPCMainThread extends Plugin {
  override async onStarting(injector: Injector): Promise<void> {
    const worker = new Worker(this._config.workerURL);
    const messageProtocol = createWebWorkerMessagePortOnMain(worker);
    const client = new ChannelClient(messageProtocol);
    const server = new ChannelServer(messageProtocol);
 
    const dependencies: Dependency[] = [
        IRemoteInstanceService,
        { useFactory: () => toModule<IRemoteInstanceService>(client.getChannel(RemoteInstanceServiceName)) },
      ],
    ];
    dependencies.forEach((dependency) => injector.add(dependency));
  }
}

在 Web Worker 线程中,需要实现 IRemoteInstanceService 接口,然后将其注册到 ChannelServer 中,这样 Web Worker 线程的 RPC 模块就可以将主线程中的 RPC 调用转发到 Web Worker 线程中的 IRemoteInstanceService 实现。

/**
 * This plugin is used to register the RPC services on the worker thread.
 */
export class UniverRPCWorkerThreadPlugin extends Plugin {
  override onStarting(injector: Injector): void {
    const messageProtocol = createWebWorkerMessagePortOnWorker();
    const server = new ChannelServer(messageProtocol);
 
    const dependencies: Dependency[] = [
      [IRemoteInstanceService, { useClass: RemoteInstanceReplicaService }],
    ];
    dependencies.forEach((dependency) => injector.add(dependency));
    server.registerChannel(RemoteInstanceServiceName, fromModule(injector.get(IRemoteInstanceService)));
  }
}

实际上,@univerjs/rpc 插件提供的 RPC 机制可以用于位于任意两个环境中的 Univer 实例之间的通信,不仅限于主线程和 Web Worker,所以实现服务端计算或者是基于 Electron 的多进程也是简单易行的,只需要实现对应环境中的通信机制(即 messageProtocol)即可。