I've got some heavy canvas code that I want to offload into a WebWorker. When I follow the examples in this page and I pass the path to the worker typescript file into the constructor
New Worker("./PaintCanvas.ts")
the code successfully compiles but when it runs the worker doesn't seem to have found the code correctly because it throws an error saying Uncaught SyntaxError: Unexpected token '<'
and the file it appears to be attempting to execute is actually my index.html file.
This is the component that I'm trying to run the worker from:
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;
And this is the worker that I'm trying to load:
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);
}
}
}
}
}
As you can see my worker will also be creating its own workers once this works properly.
You should also notice the workerPath import at the top. I tried implementing the top answer from this similar stackoverflow question from a few years ago, and that did make the code itself visible but it wouldn't work as long as the code remains a typescript module. Instead I tried just instantiating the worker using the constructor of the class, eg new PaintCanvas()
, but that still required a url and it still only finds the index.html file.
I also tried putting the worker file in the react public folder and using the public url to reference it. The tsconfig file automatically noticed this and added the file to the "include" section, which I thought looked promising, but it still just tried to execute index.html.
So my question is, is there an idiomatic non-hacky way to implement Web Workers in typescript in react or should I use one of the hacky methods I've seen elsewhere? I know that the Web Worker API is newly matured so hopefully the workarounds that a lot of the answers I'm finding online offer up aren't necessary anymore.
The problem with import * as workerPath from "file-loader?name=[name].js!./PaintCanvas";
is that file-loader
doesn't transform the file you reference, it simply copies it over to your output
directory, so you're dealing with an untranspiled file in that case.
In the this.worker = new Worker("./PaintCanvas.ts");
webpack doesn't see the import as an import and so your code is run as-is and the ./PaintCanvas.ts
URL hits your web server and it serves up whatever it wants since there isn't a static asset there (in this case, I'm assuming webpack-dev-server it's hitting the catch-all HTML for the index
page).
What you need to do is pull in worker-loader
so you can still have your code pass through the rest of the pipeline, but properly load it as a web worker:
import PaintCanvasWorker from 'worker-loader!./PaintCanvas';
// ... later ...
this.worker = new PaintCanvasWorker();