Shared Worker in JavaScript

Shared Worker in JavaScript

Questo articolo spiega il Shared Worker in JavaScript.

Spiegheremo tutto dai fondamenti di Shared Worker fino ai casi d’uso pratici passo dopo passo.

YouTube Video

javascript-shared-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <script>
111        // Override console.log to display messages in the #output element
112        (function () {
113            // Override console.log
114            const originalLog = console.log;
115            console.log = function (...args) {
116                originalLog.apply(console, args);
117                const message = document.createElement('div');
118                message.textContent = args.map(String).join(' ');
119                output.appendChild(message);
120            };
121
122            // Override console.error
123            const originalError = console.error;
124            console.error = function (...args) {
125                originalError.apply(console, args);
126                const message = document.createElement('div');
127                message.textContent = args.map(String).join(' ');
128                message.style.color = 'red'; // Color error messages red
129                output.appendChild(message);
130            };
131        })();
132
133        document.getElementById('executeBtn').addEventListener('click', () => {
134            // Prevent multiple loads
135            if (document.getElementById('externalScript')) return;
136
137            const script = document.createElement('script');
138            script.src = 'javascript-shared-worker.js';
139            script.id = 'externalScript';
140            //script.onload = () => console.log('javascript-shared-worker.js loaded and executed.');
141            //script.onerror = () => console.log('Failed to load javascript-shared-worker.js.');
142            document.body.appendChild(script);
143        });
144    </script>
145</body>
146</html>

Shared Worker in JavaScript

Che cos'è un Shared Worker?

Un Shared Worker è un thread worker che può essere condiviso tra più pagine (schede, finestre, iframe, ecc.) all'interno dello stesso origin. A differenza di un Dedicated Worker specifico per una pagina, la caratteristica principale è che un singolo processo in background può essere condiviso tra più pagine. Gli utilizzi tipici includono i seguenti:.

  • Puoi condividere una singola connessione WebSocket tra più schede, riducendo il numero di connessioni e centralizzando la gestione delle riconnessioni.
  • Puoi sincronizzare lo stato tra le schede (gestione centralizzata per Pub/Sub o store).
  • Puoi serializzare le operazioni IndexedDB, mediando l'accesso simultaneo.
  • Puoi evitare l'esecuzione duplicata di processi computazionalmente costosi.

Shared Worker ha un ruolo diverso rispetto al Service Worker. Service Worker agisce principalmente come proxy di rete, mentre Shared Worker si concentra sull'esecuzione di calcoli arbitrari o sulla gestione dello stato condiviso tra più pagine.

API di base e ciclo di vita

Creazione (Thread principale)

 1// main.js
 2// The second argument 'name' serves as an identifier to share the same SharedWorker
 3// if both the URL and name are the same.
 4const worker = new SharedWorker('/shared-worker.js', { name: 'app-core' });
 5
 6// Get the communication port (MessagePort) with this SharedWorker
 7const port = worker.port;
 8
 9// Receive messages with onmessage, send messages with postMessage
10port.onmessage = (ev) => {
11    console.log('From SharedWorker:', JSON.stringify(ev.data));
12};
13
14// When using onmessage, start() is not required
15// (start() is needed when using addEventListener instead)
16port.postMessage({ type: 'hello', from: location.pathname });
  • Questo codice mostra come creare un Shared Worker e inviare/ricevere messaggi tramite la sua porta.

Ricevitore (all'interno dello scope del Shared Worker)

 1// shared-worker.js
 2// SharedWorkerGlobalScope has an onconnect event,
 3// and a MessagePort is provided for each connection
 4const ports = new Set();
 5
 6onconnect = (e) => {
 7    const port = e.ports[0];
 8    ports.add(port);
 9
10    port.onmessage = (ev) => {
11        // Log the received message and send a reply back to the sender
12        console.log('From page:', ev.data);
13        port.postMessage({ type: 'ack', received: ev.data });
14    };
15
16    // For handling explicit disconnection from the page (via MessagePort.close())
17    port.onmessageerror = (err) => {
18        console.error('Message error:', err);
19    };
20
21    // Greeting immediately after connection
22    port.postMessage({ type: 'welcome', clientCount: ports.size });
23};
24
25// Explicit termination of the SharedWorker is usually unnecessary
26// (It will be garbage collected when all ports are closed)
  • Questo codice dimostra come, all’interno del Shared Worker, viene ricevuta una MessagePort per ogni connessione e come gestire e rispondere ai messaggi dai client.

Ciclo di vita di un Shared Worker

Shared Worker inizia quando viene stabilita la prima connessione e può terminare quando l'ultima porta viene chiusa. Le connessioni vengono chiuse al ricaricamento/chiusura della pagina e il worker viene ricreato se necessario.

Shared Worker è uno "script di lunga durata condiviso all'interno del browser". Se non si presta attenzione al ciclo di vita del Shared Worker, è più probabile che si verifichino problemi come perdite di risorse, persistenza di stati obsoleti e riavvii indesiderati.

Prova: Broadcast tra schede

Questa è un’implementazione minimale in cui più schede si connettono allo stesso Shared Worker e qualsiasi messaggio inviato viene trasmesso a tutte le schede.

main.js

 1const worker = new SharedWorker('/shared-worker.js', { name: 'bus' });
 2const port = worker.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'message') {
 7        const li = document.createElement('li');
 8        li.textContent = `[${new Date(msg.at).toLocaleTimeString()}] ${msg.payload}`;
 9        document.querySelector('#messages').appendChild(li);
10    } else if (msg.type === 'ready') {
11        console.log(`Connected. clients=${msg.clients}`);
12    }
13};
14
15document.querySelector('#form').addEventListener('submit', (e) => {
16    e.preventDefault();
17    const input = document.querySelector('#text');
18    port.postMessage({ type: 'publish', payload: input.value });
19    input.value = '';
20});
21
22document.querySelector('#ping').addEventListener('click', () => {
23    port.postMessage({ type: 'ping' });
24});
  • Questo codice si connette a un Shared Worker, implementando la ricezione/visualizzazione dei messaggi, l’invio da un modulo e l’invio di un ping facendo clic su un bottone.

shared-worker.js

 1// Minimal pub/sub bus in a SharedWorker
 2const subscribers = new Set();
 3
 4/** Broadcast to all connected ports */
 5function broadcast(message) {
 6    for (const port of subscribers) {
 7        try {
 8            port.postMessage(message);
 9        } catch (e) {
10            // Unsubscribe if sending fails, just in case
11            subscribers.delete(port);
12        }
13    }
14}
15
16onconnect = (e) => {
17    const port = e.ports[0];
18    subscribers.add(port);
19
20    port.onmessage = (ev) => {
21        const msg = ev.data;
22        if (msg && msg.type === 'publish') {
23            broadcast({ type: 'message', payload: msg.payload, at: Date.now() });
24        } else if (msg && msg.type === 'ping') {
25            port.postMessage({ type: 'pong', at: Date.now() });
26        }
27    };
28
29    port.postMessage({ type: 'ready', clients: subscribers.size });
30};
  • Questo codice implementa una semplice funzione pub/sub all’interno del Shared Worker per inoltrare messaggi tra più client.

index.html

 1<!doctype html>
 2<html>
 3    <body>
 4        <form id="form">
 5            <input id="text" placeholder="say something" />
 6            <button>Send</button>
 7        </form>
 8        <button id="ping">Ping</button>
 9        <ul id="messages"></ul>
10        <script src="/main.js" type="module"></script>
11    </body>
12</html>
  • Se apri questa pagina in più schede, i messaggi inviati da una qualsiasi scheda verranno notificati anche alle altre.

Design del messaggio: Request/Response e Correlation ID

Quando più client interagiscono con un Shared Worker, spesso si vuole identificare a quale richiesta corrisponde ciascuna risposta. Pertanto, è prassi comune includere un ID di correlazione per associare le risposte alle richieste.

main.js

 1function createClient(workerUrl, name) {
 2    const w = new SharedWorker(workerUrl, { name });
 3    const port = w.port;
 4    const pending = new Map(); // id -> {resolve,reject}
 5    let nextId = 1;
 6
 7    port.onmessage = (ev) => {
 8        const { id, ok, result, error } = ev.data || {};
 9        if (!id || !pending.has(id)) return;
10        const { resolve, reject } = pending.get(id);
11        pending.delete(id);
12        ok ? resolve(result) : reject(new Error(error));
13    };
14
15    function call(method, params) {
16        const id = nextId++;
17        return new Promise((resolve, reject) => {
18            pending.set(id, { resolve, reject });
19            port.postMessage({ id, method, params });
20        });
21    }
22
23    return { call };
24}
25
26// usage
27const client = createClient('/shared-worker.js', 'rpc');
28client.call('add', { a: 2, b: 3 }).then(console.log);   // -> 5
29client.call('sleep', { ms: 500 }).then(console.log);     // -> 'done'
  • Questo codice implementa un semplice client RPC che può effettuare chiamate asincrone di metodi verso il Shared Worker. Qui, un server RPC (Remote Procedure Call) si riferisce a un server che fornisce un meccanismo per chiamare funzioni e procedure da altri programmi o processi.
  • In questo esempio, l'id viene semplicemente incrementato, ma si può anche utilizzare un UUID, una stringa casuale, oppure combinare un timestamp con un contatore per creare una chiave univoca.

shared-worker.js

 1// A small request/response router with correlation ids
 2onconnect = (e) => {
 3    const port = e.ports[0];
 4
 5    port.onmessage = async (ev) => {
 6        const { id, method, params } = ev.data || {};
 7        try {
 8            let result;
 9            switch (method) {
10                case 'add':
11                    result = (params?.a ?? 0) + (params?.b ?? 0);
12                    break;
13                case 'sleep':
14                    await new Promise(r => setTimeout(r, params?.ms ?? 0));
15                    result = 'done';
16                    break;
17                default:
18                    throw new Error(`Unknown method: ${method}`);
19            }
20            port.postMessage({ id, ok: true, result });
21        } catch (err) {
22            port.postMessage({ id, ok: false, error: String(err) });
23        }
24    };
25};
  • Questo codice implementa un semplice server RPC che esegue il processo secondo il method e restituisce il risultato insieme all'ID della richiesta.

Pattern di utilizzo tipici

Esistono diversi pattern pratici nell’utilizzo di Shared Worker, come i seguenti:.

Multiplexing di una singola WebSocket (condivisa tra schede)

Se ogni scheda apre la propria connessione WebSocket, ciò influenzerà il carico del server e il limite di connessioni. Posiziona solo una WebSocket nel Shared Worker, e ogni scheda invia/riceve messaggi tramite il worker.

main.js

 1const w = new SharedWorker('/shared-worker.js', { name: 'ws' });
 2const port = w.port;
 3
 4port.onmessage = (ev) => {
 5    const msg = ev.data;
 6    if (msg.type === 'data') {
 7        console.log('Server says:', msg.payload);
 8    } else if (msg.type === 'socket') {
 9        console.log('WS state:', msg.state);
10    }
11};
12
13function send(payload) {
14    port.postMessage({ type: 'send', payload });
15}
  • Questo codice implementa il processo di invio/ricezione di messaggi verso un server WebSocket e la ricezione delle notifiche di stato della connessione tramite un Shared Worker.

shared-worker.js

 1let ws;
 2const ports = new Set();
 3
 4function ensureSocket() {
 5    if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
 6        return;
 7    }
 8    ws = new WebSocket('wss://example.com/stream');
 9    ws.onopen = () => broadcast({ type: 'socket', state: 'open' });
10    ws.onclose = () => broadcast({ type: 'socket', state: 'closed' });
11    ws.onerror = (e) => broadcast({ type: 'socket', state: 'error', detail: String(e) });
12    ws.onmessage = (m) => broadcast({ type: 'data', payload: m.data });
13}
14
15function broadcast(msg) {
16    for (const p of ports) p.postMessage(msg);
17}
18
19onconnect = (e) => {
20    const port = e.ports[0];
21    ports.add(port);
22    ensureSocket();
23
24    port.onmessage = (ev) => {
25        const msg = ev.data;
26        if (msg?.type === 'send' && ws?.readyState === WebSocket.OPEN) {
27            ws.send(JSON.stringify(msg.payload));
28        }
29    };
30};
  • Questo codice implementa un meccanismo all’interno del Shared Worker per condividere una singola connessione WebSocket, trasmettendo lo stato della connessione e i dati ricevuti a tutti i client, mentre invia le richieste in uscita di ogni client al server tramite la WebSocket.

  • Centralizzando nel Shared Worker sia le strategie di riconnessione sia la coda dei messaggi non inviati, il comportamento diventa coerente tra tutte le schede.

Mediazione IndexedDB (Serializzazione)

Quando si accede allo stesso DB da più schede, se vuoi evitare conflitti da transazioni simultanee o attese di lock, puoi metterle in coda nel Shared Worker e gestirle in modo seriale.

 1// db-worker.js (very simplified – error handling omitted)
 2let db;
 3async function openDB() {
 4    if (db) return db;
 5    db = await new Promise((resolve, reject) => {
 6        const req = indexedDB.open('appdb', 1);
 7        req.onupgradeneeded = () => req.result.createObjectStore('kv');
 8        req.onsuccess = () => resolve(req.result);
 9        req.onerror = () => reject(req.error);
10    });
11    return db;
12}
13
14async function put(key, value) {
15    const d = await openDB();
16    return new Promise((resolve, reject) => {
17        const tx = d.transaction('kv', 'readwrite');
18        tx.objectStore('kv').put(value, key);
19        tx.oncomplete = () => resolve(true);
20        tx.onerror = () => reject(tx.error);
21    });
22}
23
24async function get(key) {
25    const d = await openDB();
26    return new Promise((resolve, reject) => {
27        const tx = d.transaction('kv', 'readonly');
28        const req = tx.objectStore('kv').get(key);
29        req.onsuccess = () => resolve(req.result);
30        req.onerror = () => reject(req.error);
31    });
32}
33
34onconnect = (e) => {
35    const port = e.ports[0];
36    port.onmessage = async (ev) => {
37        const { id, op, key, value } = ev.data;
38        try {
39            const result = op === 'put' ? await put(key, value) : await get(key);
40            port.postMessage({ id, ok: true, result });
41        } catch (err) {
42            port.postMessage({ id, ok: false, error: String(err) });
43        }
44    };
45};
  • Questo codice implementa un meccanismo in cui le operazioni di lettura/scrittura su IndexedDB vengono messe in coda tramite il Shared Worker, serializzando l’accesso da più schede per evitare conflitti e attesa per i lock.

Suggerimenti per modularizzazione e bundler

Se desideri scrivere lo script del tuo Shared Worker come modulo ES, il comportamento e il supporto possono variare a seconda dell’ambiente. In pratica, è più sicuro scegliere una delle seguenti opzioni:.

  • Scrivi nel formato worker classico e utilizza importScripts() per caricare le dipendenze se necessario.
  • Utilizza la funzionalità di entry worker del tuo bundler (come Vite / Webpack / esbuild, ecc.) e crea un bundle separato per il Shared Worker in fase di build.

Suggerimenti per gestione degli errori, rilevamento delle disconnessioni e robustezza

Considera i seguenti punti per la gestione degli errori e la robustezza:.

  • Gestione degli invii prima che la connessione sia stabilita Metti in coda i messaggi che arrivano prima che la porta sia pronta.

  • Rilevamento disconnessione MessagePort non dispone di un handler onclose standard. Dal lato principale, invia un messaggio {type: 'bye'} con port.postMessage durante l'evento beforeunload, oppure specifica chiaramente nel protocollo per assicurarti che il worker venga chiuso correttamente.

  • Riconnessione Quando una pagina viene ricaricata o una scheda riaperta, viene creata una nuova porta. Prepara un messaggio di sincronizzazione iniziale (per inviare lo stato completo subito).

  • Backpressure Durante broadcast intensi, passa a throttling/debouncing o invio di snapshot.

  • Sicurezza Un Shared Worker è fondamentalmente condiviso all’interno dello stesso origin. Se inserisci segreti all’interno del worker, considera di progettare una verifica lato chiamante tramite token o metodi simili.

Come utilizzare correttamente Dedicated Worker, Shared Worker e Service Worker

Ogni tipo di Worker ha le seguenti caratteristiche:.

  • Dedicated Worker Dedicated Worker è destinato all'uso da parte di una sola pagina. Permette una separazione 1:1 dei calcoli rispetto all'interfaccia utente.

  • Shared Worker Shared Worker può essere condiviso tra più pagine con la stessa origine. È ideale per la comunicazione tra tab e la condivisione di una singola connessione.

  • Service Worker Service Worker può essere utilizzato per il proxy di rete, la cache, operazioni offline, notifiche push e sincronizzazione in background. Il suo punto di forza è la capacità di intercettare le richieste fetch.

Come regola generale: usa Shared Worker per la 'condivisione di informazioni e elaborazioni arbitrarie tra schede', Service Worker per il 'controllo della rete' e Dedicated Worker se vuoi semplicemente spostare elaborazioni pesanti dall'interfaccia utente.

Errori comuni

Quando usi un Shared Worker, devi fare attenzione ai seguenti aspetti.

  • Dimenticare di chiamare start() o invocazioni non necessarie Quando utilizzi port.addEventListener('message', ...), devi chiamare port.start(). Questo non è necessario se usi port.onmessage = ....

  • Broadcasting non limitato Il carico aumenta con l’aumentare delle tab aperte. Puoi ridurre il carico implementando la consegna differenziale o argomenti di sottoscrizione (filtraggio per argomento).

  • Costo di copia degli oggetti postMessage duplica i dati. Per dati di grandi dimensioni, considera l'invio come Transferable (ad esempio ArrayBuffer) o l'uso di memoria condivisa (SharedArrayBuffer) insieme a Atomics.

  • Durata e reinizializzazione Shared Worker può terminare quando l’ultimo client si disconnette. Progetta adeguatamente l’inizializzazione per le prime connessioni e per i riavvii per evitare effetti collaterali e bug.

Riepilogo

  • Shared Worker è il luogo per implementare logiche personalizzate durature condivise tra più pagine.
  • Chiarisci il contratto di messaggi (tipi/protocolli) e rendi robusto il design delle richieste/risposte aggiungendo correlation ID.
  • È ideale per processi che funzionano meglio se unificati tra tutte le tab, come il multiplexing delle connessioni WebSocket o la serializzazione dell’accesso a IndexedDB.
  • La cura di dettagli come bundler, definizioni di tipo e riconnessione può migliorare notevolmente la manutenibilità e l’esperienza dell’utente.

Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.

YouTube Video