在现代前端项目中使用 Worker

本文最后更新于:2021年1月26日 早上

场景

由于需要在 Browser 进行大量的(音频转解码)计算,所以吾辈开始尝试使用 webworker 分离 CPU 密集型的计算操作,最终找到了 comlink 这个库,但之前在 vue 中使用时发生了错误,目前看起来已经得到了解决,所以在此记录一下。

调研方案

  • web-worker-proxy:结合了 proxy/promise/webworker 的强大工具库,但如何在 ts 中使用却是个问题
  • Orc.js:一个简单的 worker 封装
  • VueWorker:结合 vue 的 worker 封装,无法理解,难道真的会有人在 vue 组件中进行大量计算么?
  • comlink:Chrome 的一个基于 proxy/promise/webworker 的封装库
  • worker-plugin:和上面的同属 chrome 实验室的一个 webpack 插件

最后决定使用 comlink 结合 worker-plugin 实现简单的 worker 使用。

安装与配置

在 GitHub 上有 可运行示例 demo
相关问题:comlink-loader 工作不正常

添加相关依赖

1
2
yarn add comlink
yarn add -D worker-plugin

在 webpack 配置中添加插件

1
2
3
{
plugins: [new WorkerPlugin()];
}

这里一般不需要特殊参数配置,如果需要,可以参考:worker-plugin

示例

基本示例

添加一个简单的 hello.worker.ts

1
2
3
4
5
6
7
8
9
10
import { expose } from "comlink";

const obj = {
counter: 0,
inc() {
this.counter++;
},
};

expose(obj);

main.ts 中使用

1
2
3
4
const obj = wrap(new Worker("./hello.worker.ts", { type: "module" })) as any;
alert(`Counter: ${await obj.counter}`);
await obj.inc();
alert(`Counter: ${await obj.counter}`);

但这里并不是类型安全的,所以我们可以实现正确的类型。

添加一个 hello.worker.ts 暴露出来的类型 HelloWorkerType

1
2
3
4
export interface HelloWorkerType {
counter: number;
inc(): void;
}

同时为了支持在 main.ts 中使用正确的类型,需要使用泛型

main.ts 修改如下

1
2
3
4
5
6
const obj = wrap<HelloWorkerType>(
new Worker("./hello.worker.ts", { type: "module" })
);
alert(`Counter: ${await obj.counter}`);
await obj.inc();
alert(`Counter: ${await obj.counter}`);

纯函数

声明函数的类型 HelloCallback.worker.type.d.ts

1
2
3
4
5
6
type ListItem<T extends any[]> = T extends (infer U)[] ? U : never;

export type MapWorkerType = <List extends any[], U>(
arr: List,
cb: (val: ListItem<List>) => U | Promise<U>
) => Promise<U[]>;

声明一个纯函数 HelloCallback.worker.ts

1
2
3
4
5
6
import { MapWorkerType } from "./HelloCallback.worker.type";
import { expose } from "comlink";

export const map: MapWorkerType = (arr, cb) => Promise.all(arr.map(cb));

expose(map);

注:此处最好使用变量的形式,主要是为了方便将函数类型剥离出去。

main.ts 中使用

1
2
3
4
5
6
7
8
9
10
const map = wrap<MapWorkerType>(
new Worker("./HelloCallback.worker.ts", {
type: "module",
})
);
const list = await map(
[1, 2, 3],
proxy((i) => i * 2)
);
console.log("list: ", list);

使用 class 的形式

声明接口 HelloClass.worker.type.d.ts

1
2
3
export class HelloClassWorker {
sum(...args: number[]): number;
}

worker 文件 HelloClass.worker.ts

1
2
3
4
5
6
7
8
9
10
import { HelloClassWorker } from "./HelloClass.worker.type";
import { expose } from "comlink";

class HelloClassWorkerImpl implements HelloClassWorker {
sum(...args: number[]): number {
return args.reduce((res, i) => res + i, 0);
}
}

expose(HelloClassWorkerImpl);

关于此处 implements class 的问题,吾辈偶然一试之下没报错也很奇怪,所以找到了相关问题 Typescript: How to extend two classes?,官方文档也同样说明了这个特性 Mixins

main.ts 中使用

1
2
3
4
5
6
7
const HelloClassWorkerClazz = wrap<typeof HelloClassWorker>(
new Worker("./HelloClass.worker.ts", {
type: "module",
})
);
const instance = await new HelloClassWorkerClazz();
console.log(await instance.sum(1, 2));

总结

总的来说,使用 worker 的基本分三步

  1. 编写需要放在 worker 里内容的类型定义
  2. 根据类型定义实现它
  3. 在主进程的代码中使用它

注:当然,如果是复杂的东西,可以直接在单独的文件中实现,然后声明一个 .worker.ts 暴露出去,不在 .worker.ts 中包含任何

参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!