如何使用 Typescript 在 React 中使用 WebWorker

How to use WebWorkers in React using Typescript

我有一些繁重的 canvas 代码,我想卸载到 WebWorker 中。当我按照 this page 中的示例并将工作打字稿文件的路径传递给构造函数时 New Worker("./PaintCanvas.ts") 代码成功编译,但是当它 运行s 时,工作人员似乎没有正确找到代码,因为它抛出一个错误,说 Uncaught SyntaxError: Unexpected token '<' 并且它似乎正在尝试的文件执行实际上是我的 index.html 文件。
这是我正在尝试 运行 工作人员的组件:

import React, { RefObject } from 'react';
//eslint-disable-next-line import/no-webpack-loader-syntax
import * as workerPath from "file-loader?name=[name].js!./PaintCanvas";
import './Canvas.css';

interface IProps {

}

interface IState {

}

class Canvas extends React.Component<IProps, IState> {
    private canvasRef: RefObject<HTMLCanvasElement>;
    private offscreen?: OffscreenCanvas;
    private worker?: Worker;

    constructor(props: IProps) {
        super(props);
        this.canvasRef = React.createRef();

        this.resizeListener = this.resizeListener.bind(this);

        window.addEventListener('resize', this.resizeListener);
    }

    componentDidMount() {
        let canvas = this.canvasRef.current;
        if (canvas) {
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight;
            this.offscreen = canvas.transferControlToOffscreen();
            this.worker = new Worker("./PaintCanvas.ts");
            this.worker.postMessage(this.offscreen, [this.offscreen]);
        }

    }

    resizeListener() {
        let canvas = this.canvasRef.current;
        if (canvas) {
            canvas.width = window.innerWidth > canvas.width ? window.innerWidth : canvas.width;
            canvas.height = window.innerHeight > canvas.height ? window.innerHeight : canvas.height;
            this.offscreen = canvas.transferControlToOffscreen();
            this.worker = new Worker("./PaintCanvas.ts");
            this.worker.postMessage(this.offscreen, [this.offscreen]);
        }
    }

    render() {
        return (
            <>
                <canvas className="noiseCanvas" ref={this.canvasRef}/>
                <div className="overlay">
                </div>
            </>
        );
    }
}

export default Canvas;

这是我要加载的工作器:

export default class PaintCanvas extends Worker {
    private canvas?: OffscreenCanvas;
    private intervalId?: number;
    private frame: number;
    private frameSet: number;
    private noiseData: ImageData[][];
    private noiseNum: number[];
    private overlayFrame: number[];
    private overlayData: Uint8ClampedArray[][];
    private overlayNum: number[];
    private workers: (Worker|undefined)[];

    constructor(stringUrl: string | URL) {
        super(stringUrl);
        this.frame = 0;
        this.frameSet = 0;
        this.noiseData = [[], [], []];
        this.noiseNum = [0, 0, 0];
        this.overlayFrame = [0, 0, 0];
        this.overlayData = [[], [], []];
        this.overlayNum = [0, 0, 0];

        this.workers = [undefined, undefined, undefined];
    }

    onmessage = (event: MessageEvent) => {
        this.canvas = event.data;
        this.frame = 0;
        this.frameSet = 0;
        this.noiseData = [[], [], []];
        this.noiseNum = [0, 0, 0];
        this.overlayFrame = [0, 0, 0];
        this.overlayData = [[], [], []];
        this.overlayNum = [0, 0, 0];
        if (this.workers[0]) {
            this.workers[0].terminate();
        }
        if (this.workers[1]) {
            this.workers[1].terminate();
        }
        if (this.workers[2]) {
            this.workers[2].terminate();
        }
        this.makeNoise(0);
        this.makeNoise(1);
        this.makeNoise(2);
        if (this.intervalId) {
            window.clearInterval(this.intervalId);
        }
        this.intervalId = window.setInterval(this.paintNoise, 100);
    }

    makeNoise(index: number) {
        if (this.canvas) {
            const width = this.canvas.width;
            const height = this.canvas.height;
            this.workers[index] = new Worker("./FillCanvas.ts");
            if (this.workers[index]) {
                this.workers[index]!.onmessage = (event) => {
                    if (this.overlayNum[index] < 4 || !event.data[0]) {
                        this.overlayData[index].push(event.data);
                        this.overlayNum[index]++;
                        if (this.overlayNum[index] < 4) {
                            this.workers[index]!.postMessage([width, height]);
                        } else {
                            this.workers[index]!.postMessage([width, height, new Uint8ClampedArray(width * height * 4), this.overlayData[index][0]]);
                            this.overlayFrame[index]++;
                        }
                    } else {
                        if (event.data[0]) {
                            this.noiseData[index].push(new ImageData(event.data[0], width, height));
                            this.noiseNum[index]++;
                            if (this.noiseNum[index] < 30) {
                                this.workers[index]!.postMessage([width, height, event.data[1], this.overlayData[index][Math.ceil(this.overlayFrame[index] / 4) % 4]]);
                                this.overlayFrame[index]++;
                            } else {
                                this.workers[index] = undefined;
                            }
                        }
                    }
                }
                this.workers[index]!.postMessage([width, height]);
            }
        }
    }

    paintNoise() {
        if (this.noiseNum[0] > 10) {
            this.frame++;
            if (this.frame % this.noiseNum[this.frameSet % 3] === 0) {
                this.frameSet++;
            }
            if (this.canvas) {
                let ctx = this.canvas.getContext("2d");
                if (ctx) {
                    ctx.putImageData(this.noiseData[this.frameSet % 2][this.frame % this.noiseNum[this.frameSet % 2]], 0, 0);
                }
            }
        }
    }
}

如您所见,一旦正常工作,我的工作器也将创建自己的工作器。
您还应该注意到顶部的 workerPath 导入。几年前,我尝试实现 的最佳答案,这确实使代码本身可见,但只要代码仍然是打字稿模块,它就不会工作。相反,我尝试使用 class 的构造函数实例化工作程序,例如 new PaintCanvas(),但这仍然需要 url,它仍然只能找到 index.html 文件。
我还尝试将工作文件放在 react public 文件夹中并使用 public url 来引用它。 tsconfig 文件自动注意到这一点并将文件添加到 "include" 部分,我认为这看起来很有希望,但它仍然只是尝试执行 index.html.
所以我的问题是,是否有一种惯用的非 hacky 方式来在 typescript 中实现 Web Worker 以做出反应,或者我应该使用我在其他地方看到的一种 hacky 方法吗?我知道 Web Worker API 刚刚成熟,所以希望解决方法不再需要我在网上找到的很多答案。

import * as workerPath from "file-loader?name=[name].js!./PaintCanvas"; 的问题在于 file-loader 不会转换您引用的文件,它只是将其复制到您的 output 目录,因此您正在处理一个在这种情况下未转译的文件。

this.worker = new Worker("./PaintCanvas.ts"); 中,webpack 不会将导入视为导入,因此您的代码是 运行 原样并且 ./PaintCanvas.ts URL 访问您的网站服务器,它提供它想要的任何东西,因为那里没有静态资产(在这种情况下,我假设 webpack-dev-server 它正在为 index 页面击中全部 HTML ).

您需要做的是拉入 worker-loader 这样您仍然可以让您的代码通过管道的其余部分,但作为网络工作者正确加载它:

import PaintCanvasWorker from 'worker-loader!./PaintCanvas';

// ... later ...
this.worker = new PaintCanvasWorker();

2022 年更新:

Webpack 5 使用新的 resolve syntax:

大大简化了这项任务
// src/App.tsx
// --snip--

const worker = new Worker(new URL('./path/to/worker', import.meta.url));
worker.onmessage = (e: MessageEvent<string>) => {
    console.log('Received from worker:', e.data);
};
worker.postMessage('I love dogs');

// --snip--
// src/worker.ts
const self = globalThis as unknown as DedicatedWorkerGlobalScope;

self.onmessage = (e: MessageEvent<string>) => {
    console.log('Worker received:', e.data)
    self.postMessage(e.data + ' and cats');
};

输出:

> Worker received: I love dogs
> Received from worker: I love dogs and cats