Übersicht

Ich habe daran gearbeitet FidgetMap schon eine Weile. Ich denke, ich könnte viele Prozesse (einschließlich der grundlegenden Renderingschleife) beschleunigen, indem ich Web-ArbeiterInnen um einen Großteil der Rendering-Aufgaben asynchron zu erledigen. Das gesamte Spiel wird gezeichnet, indem RGB-Daten in Arrays geschrieben und dann auf eine LeinwandEs ist also ein perfekter Kandidat für Web-ArbeiterInnen. Es ist ziemlich verrückt zu denken, dass es so weit gekommen ist, ohne dass es einen Hintergrund-Thread gab, aber hier sind wir.

Ich werde keine ganze Geschichtsstunde über Web Worker halten, aber es reicht zu sagen, dass ich damit viele Rendering-Aufgaben an einen Hintergrund-Thread (in meinem Fall mehrere Hintergrund-Threads) übergeben kann, um den Hauptausführungs-Thread zu entlasten. Das ganze Spiel sollte schneller sein, wenn bestimmte Aufgaben im Hintergrund ausgeführt werden.

Der Hauptteil des Spiels wird direkt auf eine Leinwandwie oben erwähnt, aber die UI-Steuerelemente wie das Menü und die interaktiven Schaltflächen sind in React geschrieben. Es macht keinen Sinn, das Rad in Bezug auf Text und Flex-Style-Rendering neu zu erfinden. Also habe ich das Projekt mit create-react-app (mit Typescript). Ich möchte meine Web Worker im gleichen Stil schreiben, am besten im gleichen Quellcode. Aber du kannst nicht einfach Web-Worker-Module importieren und sie dann zu Workern machen. Du musst einen Worker mit einer URL zu einer separaten JavaScript-Datei (oder einem Bundle) instanziieren.

CRA ist großartig. Es verbirgt jede Konfiguration und macht es super einfach, ein React-Projekt zu starten. Aber es ist nicht so toll für fortgeschrittene Konfigurationen wie diese, bei denen du deine App in eine JS-Datei und möglicherweise mehrere Web Worker in separate Dateien kompilieren musst. Die übliche Empfehlung lautet Auswerfen von create-react-app. So kannst du nach Bedarf an der Konfiguration herumschrauben, aber mit großer Macht kommt auch große Verantwortung. Sobald du das Projekt ausgeworfen hast, musst du die Konfiguration und die Skripte selbst pflegen. Es ist nicht mehr so einfach, ein gut getestetes Projekt zu aktualisieren.

Wie können wir also verschiedene Kompilierziele für verschiedene Einstiegspunkte festlegen? Parzelle zur Rettung.  ParcelJS ist ein konfigurationsfreies Build-Tool, das sofort mit Typescript funktioniert. Du sagst einfach: "Hey Parcel, diese Eingabedatei", und schon erledigt es die Sache. Genug geredet, ich zeige dir jetzt, wie ich es gemacht habe.

Die Lösung

Fangen wir ganz von vorne an. Verwende create-react-app, um ein neues Projekt mit Typescript zu starten. Gehe in dein Projektverzeichnis und erstelle eine neue App wie folgt:

npx create-react-app workers-example --template typescript

Wechsle jetzt in das Verzeichnis und lass uns Parcel installieren

cd workers-example
npm i --save-dev parcel

Rad, jetzt arbeiten wir technisch gesehen mit zwei Build-Systemen: dem, das in create-react-app eingebaut ist, und Parcel, das wir gerade hinzugefügt haben. Legen wir ein neues Verzeichnis mit dem Namen "workers" im Stammverzeichnis des Projekts an. Darin werden wir den Quellcode von workers ablegen. Er befindet sich in einem anderen Verzeichnis als src, da jedes dieser Verzeichnisse ein eigenes kleines Bundle sein wird. Wir werden dort auch einen Beispielworker ablegen.

mkdir workers
cd workers
touch sampleWorker.ts

Fügen wir etwas in unseren Beispielworker ein, damit wir sehen können, dass es funktioniert

self.onmessage = (e: MessageEvent) => {
    self.postMessage("hallo, Welt vom Worker");
};

Jetzt fügen wir ein neues Skript zu unserer package.json hinzu, damit wir Worker mit npm bauen können. Wir werden alle Worker im öffentlichen Verzeichnis in einem Unterverzeichnis erstellen, damit unsere App sie von dort aus einfach nutzen kann.

{
// ...Rest der Datei
  "Skripte": {
    // ... Rest der Skripte
    "worker:build": "parcel build --dist-dir public/workers --"
  }
}

Okay, jetzt können wir Arbeiter bauen! Führe dies aus, um die Magie zu sehen:

npm run worker:build workers/sampleWorker.ts

Parcel wird sampleWorker.ts als Einstiegspunkt verwenden. Es wird erkennen, dass es sich um Typescript handelt und das Richtige tun, ohne dass zusätzliche Plugins oder Konfigurationen erforderlich sind. Wenn der Befehl beendet ist, solltest du deinen neu erstellten Worker in public/workers zusammen mit einer Sourceemap finden. Super!

Sehen Sie es in Aktion

Aktualisieren wir die automatische App-Datei, damit wir unseren neuen Worker in Aktion sehen können. Kopiere diese Datei und füge sie in deine App.tsx ein, um sie mit dem Worker, den wir gebaut haben, zu verbinden.

import React, { FC, useCallback, useEffect, useRef } from 'react';
const App: FC = () => {
  const workerRef = useRef<worker>();
  const sendMessageToWorker = useCallback(() =&gt; {
    workerRef.current?.postMessage({});
  }, []);
  useEffect(() =&gt; {
    workerRef.current = new Worker("/workers/sampleWorker.js");
    workerRef.current?.addEventListener('message', (event) =&gt; {
      alert('Nachricht vom Worker erhalten: ' + JSON.stringify(event.data));
    });
    return () =&gt; workerRef.current?.terminate();
  }, []);
  return (
    <div>
      <button type="button" onclick="{sendMessageToWorker}">
        Nachricht an Arbeiter senden
      </button>
    </div>
  );
}
export default App;

Taking It Farther - automatisch bauen

Baue die Arbeiter, wenn die App gebaut wird

Jetzt können wir die Worker auf die einfachste Weise bauen. Es wäre besser, wenn wir die kompilierten Worker nicht zu unserem Quellcode hinzufügen, sondern sie zusammen mit dem normalen Bundle bauen würden. Das können wir ganz einfach erreichen.

Als Erstes fügen wir das öffentliche Worker-Verzeichnis (und das Cache-Verzeichnis von parcel) zu unserem .gitignore hinzu, damit wir sie nicht übertragen.

// in .gitignore
.parcel-cache
public/workers

Als Nächstes fügen wir einen Befehl hinzu, um alle Worker in deinem Worker-Verzeichnis zu bauen. Zurück in package.json...

{
// ...Rest der Datei
  "Skripte": {
    // ... Rest der Skripte
    "worker:build": "parcel build --dist-dir public/workers --",
    "worker:build:all": "npm run worker:build ./workers/*"
  }
}

Parcel rettet wieder einmal den Tag. Es verwendet jede Datei im Worker-Verzeichnis als Einstiegspunkt und erstellt für jede Datei ein eigenes Bundle. So kannst du deine Worker vollständig unterteilen. Jeder kann von node_modules importieren und diese Abhängigkeiten werden Teil des endgültigen Bundles für jeden Worker.

Aber wir wollen nicht einen separaten Build-Befehl ausführen müssen. Wir wollen, dass es zusammen mit dem einfachen npm run build ausgeführt wird. Das können wir erreichen, indem wir ein anderes Paket namens npm-run-all verwenden. Damit kannst du mehrere npm-Befehle auf einmal ausführen, entweder nacheinander oder parallel.

npm i --save-dev npm-run-all

Zurück in der package.json verschieben wir den bestehenden Build-Befehl in einen anderen Namen, damit wir den normalen Build-Befehl mehrere Dinge tun lassen können

{
// ...Rest der Datei
  "Skripte": {
    // ... Rest der Skripte
    "build": "npm-run-all --sequential workers:build:all rs:build",
    "rs:build": "react-scripts build",
    "worker:build": "parcel build --dist-dir public/workers --",
    "worker:build:all": "npm run worker:build ./workers/*"
  }
}

Alles klar! Wenn du jetzt baust, baust du zuerst alle deine Worker und führst dann den normalen react-scripts build aus, der dein create-react-app-Paket erstellt.

Jetzt kannst du deine vollständig transpilierten und gebündelten Worker überall in deinem bestehenden App-Paket verwenden, indem du das tust:

    const worker = new Worker("/workers/sampleWorker.js");

Baue die Arbeiter in die Entwicklung ein, wenn sie sich verändern

Das letzte Teil des Puzzles ist der Aufbau der Worker, wenn du `npm start` ausführst. Parcel hat einen eingebauten "watch"-Befehl, aber er setzt voraus, dass deine Datei in einer Umgebung mit einer "Window"-Variablen läuft, was bei Workern nicht der Fall ist. Deshalb können wir unseren eigenen Watcher ganz einfach mit dem Paket `node-watch` implementieren. Installiere es zunächst:

npm i --save-dev node-watch

Erstelle nun ein neues Skript namens "watchWorkers.js". Ich habe es in ein Unterverzeichnis namens "scripts" gelegt. Kopiere dies in watchWorkers.js

var watch = require('node-watch');
var { spawn } = require('child_process');
// Damit werden die Worker erstellt, wenn das Skript gestartet wird
spawn(
    'npm',
    ['run', 'workers:build'],
    { stdio: 'inherit' }
);
watch('./workers/', { recursive: true }, function(evt, name) {
    // dies baut jeden einzelnen Worker auf, sobald er aktualisiert wird
    spawn(
        'npm',
        ['run', 'worker:build', name],
        { stdio: 'inherit' }
    );
});

Jetzt haben wir ein Skript, das jeden Worker neu aufbaut, wenn er sich ändert. Ändern wir die package.json noch einmal, damit sie Teil unseres normalen Prozesses wird

{
// ...Rest der Datei
  "Skripte": {
    // ... Rest der Skripte
    "start": "npm-run-all --parallel workers:watch rs:start",
    "rs:start": "react-scripts start",
    "build": "npm-run-all --sequential workers:build:all rs:build",
    "rs:build": "react-scripts build",
    "worker:build": "parcel build --dist-dir public/workers --",
    "worker:build:all": "npm run worker:build ./workers/*",
    "workers:watch": "node ./scripts/watchWorkers.js"
  }
}

Wenn du jetzt `npm start` ausführst, wird der normale React-Skript-Start ausgeführt, der auf Änderungen im App-Bundle achtet, aber du führst auch deine eigene Worker-Watch aus, die jeden Worker bei jeder Aktualisierung neu überträgt.

Einen Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

de_ATDE_AT
Nach oben scrollen