Panoramica

Ho lavorato su FidgetMap da un po' di tempo a questa parte. Penso che potrei velocizzare molti processi (compreso il ciclo di rendering di base) aggiungendo Lavoratori web per gestire molte delle attività di rendering in modo asincrono. L'intero gioco viene disegnato scrivendo i dati RGB in array e componendoli poi su una telaquindi è davvero un candidato perfetto per Lavoratori web. È piuttosto assurdo pensare che sia arrivato a questo punto senza alcun filo conduttore, ma eccoci qui.

Non mi dilungherò in un'intera lezione di storia sui Web Worker, basti dire che mi permetteranno di passare molte operazioni di rendering a un thread in background (più thread in background nel mio caso) per liberare il thread di esecuzione principale. L'intero gioco dovrebbe essere più veloce con alcuni compiti compartimentati in background.

La parte principale del gioco è disegnata direttamente su un telama i controlli dell'interfaccia utente, come il menu e i pulsanti interattivi, sono scritti in React. Non ha senso reinventare la ruota per quanto riguarda il testo e il rendering in stile flex. Ho quindi realizzato il progetto utilizzando creare-react-app (con Typescript). Voglio scrivere i miei Web Worker nello stesso stile, preferibilmente nello stesso codice sorgente. Ma non puoi semplicemente importare i moduli Web Worker e trasformarli in worker. Devi istanziare un worker con un URL a un file (o bundle) JavaScript separato.

CRA è fantastico. Nasconde qualsiasi configurazione e rende super facile l'avvio di un progetto React. Tuttavia, non è così perfetto per le configurazioni avanzate come questa, in cui devi compilare la tua applicazione in un unico file JS e potenzialmente diversi Web Worker in file separati. Di solito si consiglia di espulsione da create-react-app. Questo ti permette di modificare la configurazione a seconda delle necessità, ma da un grande potere derivano grandi responsabilità. Una volta espulso, dovrai mantenere tu stesso la configurazione e gli script. Non è più possibile aggiornare facilmente un progetto ben collaudato.

Quindi come possiamo creare diversi target di compilazione per diversi punti di ingresso? Parcella in soccorso.  ParcelJS è uno strumento di compilazione a configurazione zero che funziona con Typescript out-of-the-box. Basta dire "hey Parcel, questo file di ingresso" e lui fa la cosa™. Basta parlare, lascia che ti mostri come ho fatto.

La soluzione

Partiamo dall'inizio. Utilizza create-react-app per iniziare un nuovo progetto con Typescript. Vai alla directory del tuo progetto e crea una nuova applicazione in questo modo:

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

Ora spostiamoci nella directory e installiamo Parcel

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

Rad, ora stiamo tecnicamente lavorando con due sistemi di compilazione: quello integrato in create-react-app e Parcel che abbiamo appena aggiunto. Creiamo una nuova cartella nella root del progetto chiamata "workers". È qui che metteremo il codice sorgente di workers. Si trova in una directory separata da src poiché ognuno di essi sarà un piccolo bundle a sé stante. Vi inseriremo anche un worker di esempio.

mkdir workers
cd workers
toccare sampleWorker.ts

Inseriamo qualcosa nel nostro worker di esempio in modo da vedere che funziona

self.onmessage = (e: MessageEvent) => {
    self.postMessage("hello, world from the worker");
};

Ora aggiungeremo un nuovo script al nostro package.json in modo da poter costruire i worker utilizzando npm. Costruiremo tutti i worker nella directory pubblica sotto una sottodirectory, in modo che la nostra applicazione possa utilizzarli facilmente da lì.

{
// ...il resto del file
  "scripts": {
    // ... il resto degli script
    "worker:build": "parcel build --dist-dir public/workers --"
  }
}

Ok, ora possiamo costruire i lavoratori! Esegui questo per vedere la magia:

npm run worker:build workers/sampleWorker.ts

Parcel utilizzerà sampleWorker.ts come punto di ingresso. Vedrà che si tratta di Typescript e farà la cosa giusta senza alcun plugin o configurazione aggiuntiva. Al termine del comando, dovresti trovare il tuo worker appena costruito in public/workers insieme a una mappa dei sorgenti. Bello!

Guardalo in azione

Aggiorniamo il file dell'App automatica in modo da poter vedere il nostro nuovo worker in azione. Copia/incolla questo file nel tuo App.tsx per interfacciarti con il lavoratore che abbiamo costruito.

import React, { FC, useCallback, useEffect, useRef } da '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('messaggio ricevuto dal lavoratore: ' + JSON.stringify(event.data));
    });
    return () =&gt; workerRef.current?.terminate();
  }, []);
  return (
    <div>
      <button type="button" onclick="{sendMessageToWorker}">
        Invia un messaggio al lavoratore
      </button>
    </div>
  );
}
export default App;

Portarlo più lontano - costruire automaticamente

Costruisce i lavoratori quando l'applicazione viene creata

Ora possiamo compilare i worker nel modo più elementare. Sarebbe meglio se non aggiungessimo i worker compilati al nostro sorgente, ma li costruissimo insieme al normale bundle. Possiamo farlo in modo abbastanza semplice.

Per prima cosa, aggiungiamo la directory dei lavoratori pubblici (e la directory della cache di parcel) al nostro .gitignore in modo da non eseguire il commit.

// in .gitignore
.parcel-cache
pubblico/lavoratori

Successivamente, aggiungeremo un comando per costruire tutti i lavoratori nella tua directory dei lavoratori. Torniamo al file package.json...

{
// ...il resto del file
  "scripts": {
    // ... il resto degli script
    "worker:build": "parcel build --dist-dir public/workers --",
    "workers:build:all": "npm run worker:build ./workers/*"
  }
}

Ancora una volta Parcel salva la situazione. Utilizzerà ogni file nella directory dei lavoratori come punto di ingresso e creerà un bundle separato per ognuno di essi. Questo ti permette di avere lavoratori completamente compartimentati. Ciascuno di essi può importare da node_modules e queste dipendenze diventeranno parte del bundle finale di ogni worker.

Ma non vogliamo dover eseguire un comando di compilazione separato. Vogliamo che lo faccia insieme al più semplice npm run build. Possiamo ottenere questo risultato utilizzando un altro pacchetto chiamato npm-run-all. Questo ti permette di eseguire più comandi npm contemporaneamente, in sequenza o in parallelo.

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

Ora, nel file package.json, sposteremo il comando di compilazione esistente con un nome diverso, in modo da poter fare in modo che il normale comando di compilazione faccia diverse cose

{
// ...il resto del file
  "scripts": {
    // ... il resto degli script
    "build": "npm-run-all --sequential workers:build:all rs:build",
    "rs:build": "react-scripts build",
    "worker:build": "parcel build --dist-dir public/workers --",
    "workers:build:all": "npm run worker:build ./workers/*"
  }
}

Bene! Ora, ogni volta che costruisci, costruisci prima tutti i tuoi worker e poi esegui il normale build di react-scripts che crea il tuo pacchetto create-react-app.

Ora puoi utilizzare i tuoi lavoratori completamente transpilati e raggruppati in qualsiasi punto del tuo pacchetto di app esistente:

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

Costruisci i lavoratori nello sviluppo ogni volta che cambiano

L'ultimo pezzo del puzzle è la creazione dei worker quando esegui `npm start`. Parcel ha un comando "watch" integrato, ma presuppone che il file sia in esecuzione in un ambiente con una variabile `window`, cosa che i worker non fanno. Possiamo quindi implementare il nostro watcher in modo semplice utilizzando un pacchetto chiamato `node-watch`. Inizia ad installarlo:

npm i --save-dev node-watch

Ora crea un nuovo script chiamato `watchWorkers.js`. Io l'ho messo in una sottodirectory chiamata "scripts". Copia questo in watchWorkers.js

var watch = require('node-watch');
var { spawn } = require('child_process');
// questo costruirà i lavoratori inizialmente quando lo script viene eseguito
spawn(
    'npm',
    ['run', 'workers:build'],
    { stdio: 'inherit' }
);
watch('./workers/', { ricorsivo: true }, function(evt, name) {
    // questo costruirà ogni singolo lavoratore man mano che viene aggiornato
    spawn(
        'npm',
        ['run', 'worker:build', name],
        { stdio: 'inherit' }
    );
});

Ora abbiamo uno script che ricostruisce ogni worker quando cambia. Ancora una volta, modifichiamo package.json per renderlo parte del nostro processo normale

{
// ...il resto del file
  "scripts": {
    // ... il resto degli script
    "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 --",
    "workers:build:all": "npm run worker:build ./workers/*",
    "workers:watch": "node ./scripts/watchWorkers.js"
  }
}

Ora, ogni volta che eseguirai `npm start`, eseguirai il normale avvio di react-scripts che verificherà le modifiche al bundle dell'app, ma eseguirai anche il tuo worker watch che effettuerà il retranspile di ogni worker man mano che viene aggiornato.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

it_ITIT
Scorri in alto