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 & 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 alService Worker
.Service Worker
agisce principalmente come proxy di rete, mentreShared 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 unaMessagePort
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 handleronclose
standard. Dal lato principale, invia un messaggio{type: 'bye'}
conport.postMessage
durante l'eventobeforeunload
, 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' eDedicated 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 utilizziport.addEventListener('message', ...)
, devi chiamareport.start()
. Questo non è necessario se usiport.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 esempioArrayBuffer
) o l'uso di memoria condivisa (SharedArrayBuffer
) insieme aAtomics
. -
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.