Shared Worker in JavaScript

Shared Worker in JavaScript

Dit artikel legt de Shared Worker in JavaScript uit.

We leggen alles stap voor stap uit, van de basisprincipes van Shared Worker tot praktische toepassingen.

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

Wat is een Shared Worker?

Een Shared Worker is een worker thread die gedeeld kan worden tussen meerdere pagina's (tabbladen, vensters, iframes, enz.) binnen dezelfde oorsprong. In tegenstelling tot een pagina-specifieke Dedicated Worker is het belangrijkste kenmerk dat één enkel achtergrondproces gedeeld kan worden tussen meerdere pagina's. Typische gebruikssituaties zijn onder meer de volgende:.

  • Je kunt één enkele WebSocket-verbinding delen tussen meerdere tabbladen, waardoor het aantal verbindingen afneemt en herverbindingen worden gecentraliseerd.
  • Je kunt de status tussen tabbladen synchroniseren (gecentraliseerd beheer voor Pub/Sub of stores).
  • Je kunt IndexedDB-bewerkingen serialiseren, waarbij gelijktijdige toegang wordt bemiddeld.
  • Je kunt dubbele uitvoering van computationeel zware processen voorkomen.

Shared Worker heeft een andere rol dan een Service Worker. Service Worker fungeert vooral als een netwerkproxy, terwijl Shared Worker zich richt op het uitvoeren van willekeurige berekeningen of statusbeheer die gedeeld zijn tussen pagina’s.

Basis-API en levenscyclus

Aanmaken (Hoofdthread)

 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 });
  • Deze code toont hoe je een Shared Worker maakt en berichten verzendt/ontvangt via zijn poort.

Ontvanger (binnen de Shared Worker scope)

 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)
  • Deze code laat zien hoe, binnen de Shared Worker, een MessagePort wordt ontvangen voor elke verbinding en hoe berichten van clients worden verwerkt en beantwoord.

Levenscyclus van een Shared Worker

Shared Worker start wanneer de eerste verbinding tot stand is gebracht en kan beëindigen wanneer de laatste poort wordt gesloten. Verbindingen worden gesloten bij het herladen/sluiten van de pagina, en de worker wordt indien nodig opnieuw aangemaakt.

Shared Worker is een "langlevend script dat wordt gedeeld binnen de browser". Als je niet let op de levenscyclus van de Shared Worker, is de kans groter dat er problemen optreden, zoals geheugendatalekken, het vasthouden van verouderde status en onbedoelde herstarts.

Probeer het: Broadcast tussen tabbladen

Dit is een minimale implementatie waarbij meerdere tabbladen verbinding maken met dezelfde Shared Worker en elk verstuurd bericht wordt naar alle tabbladen uitgezonden.

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});
  • Deze code maakt verbinding met een Shared Worker, implementeert het ontvangen/tonen van berichten, berichten verzenden via een formulier en het versturen van een ping door op een knop te klikken.

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};
  • Deze code implementeert een eenvoudige pub/sub-functionaliteit binnen de Shared Worker om berichten tussen meerdere clients door te geven.

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>
  • Als je deze pagina in meerdere tabbladen opent, worden berichten die vanuit een tabblad verstuurd zijn aan alle andere tabbladen doorgegeven.

Berichtopbouw: Request/Response en Correlation ID

Wanneer meerdere clients communiceren met een Shared Worker, wil je vaak weten welk antwoord op welk verzoek betrekking heeft. Daarom is het standaardpraktijk om een correlatie-ID op te nemen om antwoorden aan verzoeken te koppelen.

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'
  • Deze code implementeert een eenvoudige RPC-client die asynchrone methode-aanroepen naar de Shared Worker kan doen. Hier verwijst een RPC-server (Remote Procedure Call server) naar een server die een mechanisme biedt om functies en procedures aan te roepen vanuit andere programma’s of processen.
  • In dit voorbeeld wordt de id eenvoudig verhoogd, maar je kan ook een UUID, een willekeurige tekenreeks of een combinatie van een tijdstempel met een teller gebruiken om een unieke sleutel te maken.

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};
  • Deze code implementeert een eenvoudige RPC-server die verwerking uitvoert volgens de method en het resultaat samen met de aanvraag-ID terugstuurt.

Typische gebruikspatronen

Er zijn verschillende praktische patronen bij het gebruik van een Shared Worker, zoals de volgende:.

Multiplexen van een enkele WebSocket (gedeeld tussen tabbladen)

Als elk tabblad zijn eigen WebSocket-verbinding opent, heeft dat invloed op de serverbelasting en de verbindingslimiet. Plaats slechts één WebSocket in de Shared Worker, en laat elk tabblad berichten verzenden/ontvangen via de 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}
  • Deze code implementeert het proces van berichten verzenden/ontvangen naar een WebSocket-server en het ontvangen van verbindingsstatusmeldingen via een 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};
  • Deze code implementeert een mechanisme in de Shared Worker om een enkele WebSocket-verbinding te delen, waarbij verbindingsstatus en ontvangen gegevens naar alle clients worden uitgezonden, terwijl uitgaande verzoeken per client via de WebSocket naar de server worden gestuurd.

  • Door herverbindingsstrategieën en het in de wachtrij zetten van niet-verzonden berichten te centraliseren in de Shared Worker, wordt het gedrag consistent tussen alle tabbladen.

IndexedDB-mediation (serialisatie)

Wanneer je vanuit meerdere tabbladen toegang hebt tot dezelfde database en je conflicten van gelijktijdige transacties of wachttijd door locks wilt voorkomen, kun je ze in de Shared Worker in de wachtrij plaatsen en serieel verwerken.

 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};
  • Deze code implementeert een mechanisme waarbij lees/schrijf-bewerkingen naar IndexedDB via de Shared Worker in de wachtrij worden gezet, waardoor toegang vanuit meerdere tabbladen geserialiseerd wordt om conflicten en lock-problemen te voorkomen.

Modularisatie- en bundler-tips

Als je je Shared Worker-script als een ES-module wilt schrijven, kunnen gedrag en ondersteuning per omgeving verschillen. In de praktijk is het veiliger om één van de volgende opties te kiezen:.

  • Schrijf in klassiek worker-formaat en gebruik importScripts() om indien nodig afhankelijkheden te laden.
  • Gebruik de worker entry-mogelijkheid van je bundler (zoals Vite, Webpack, esbuild, enz.) en maak een aparte bundle voor de Shared Worker tijdens het buildproces.

Tips voor foutafhandeling, disconnectie-detectie en robuustheid

Let op de volgende punten voor foutafhandeling en robuustheid:.

  • Omgaan met verzonden berichten voordat de verbinding tot stand is gebracht Plaats berichten die aankomen voordat de poort klaar is in de wachtrij.

  • Detectie van disconnectie MessagePort heeft geen standaard onclose-handler. Stuur aan de hoofdkant een {type: 'bye'}-bericht met port.postMessage tijdens beforeunload, of geef dit duidelijk aan in het protocol om ervoor te zorgen dat de worker wordt opgeruimd.

  • Herverbinding Wanneer een pagina wordt herladen of een tabblad opnieuw wordt geopend, wordt er een nieuwe poort aangemaakt. Bereid een initieel synchronisatiebericht voor (om de volledige status in één keer te verzenden).

  • Backpressure Schakel bij zware broadcasts over op throttling/debouncing of het versturen van snapshots.

  • Beveiliging Een Shared Worker wordt fundamenteel gedeeld binnen dezelfde origin. Als je geheimen binnen de worker opslaat, overweeg dan om zijdelingse verificatie met tokens of vergelijkbare methoden aan de aanroepende zijde te implementeren.

Hoe u Dedicated Worker, Shared Worker en Service Worker op de juiste manier gebruikt

Elk type Worker heeft de volgende kenmerken:.

  • Dedicated Worker Dedicated Worker is bedoeld om alleen door één pagina te worden gebruikt. Het maakt een 1-op-1 scheiding van berekeningen en de gebruikersinterface mogelijk.

  • Shared Worker Shared Worker kan gedeeld worden tussen meerdere pagina’s met dezelfde oorsprong. Het is ideaal voor inter-tab communicatie en het delen van één enkele verbinding.

  • Service Worker Service Worker kan worden gebruikt voor netwerkproxy's, caching, offline-operaties, push notificaties en achtergrond-synchronisatie. De kracht ervan is het vermogen om fetch-verzoeken te onderscheppen.

Vuistregel: gebruik Shared Worker voor 'informatie-uitwisseling en willekeurige verwerking tussen tabbladen', Service Worker voor 'netwerkbeheer', en Dedicated Worker als je alleen zware verwerking van de UI wilt loskoppelen.

Veelvoorkomende valkuilen

Als u een Shared Worker gebruikt, let dan op de volgende zaken.

  • Vergeten om start() aan te roepen of onnodige aanroepen Bij gebruik van port.addEventListener('message', ...) moet u port.start() aanroepen. Dit is niet nodig als u port.onmessage = ... gebruikt.

  • Onbeperkt uitzenden De belasting neemt toe naarmate het aantal tabbladen toeneemt. U kunt de belasting verminderen door differentiële levering of abonnements-onderwerpen (filteren op onderwerp) te implementeren.

  • Kopieerkosten van objecten postMessage dupliceert de gegevens. Overweeg bij grote hoeveelheden data om die als Transferable (zoals ArrayBuffer) te verzenden of gedeeld geheugen (SharedArrayBuffer) samen met Atomics te gebruiken.

  • Levensduur en herinitialisatie Shared Worker kan worden beëindigd wanneer de laatste client de verbinding verbreekt. Ontwerp de initialisatie voor eerste verbindingen en herstarten goed om bijwerkingen en bugs te voorkomen.

Samenvatting

  • Shared Worker is bedoeld om langdurige, aangepaste logica die door meerdere pagina’s wordt gedeeld te implementeren.
  • Maak het berichtcontract (types/protocollen) duidelijk en versterk het ontwerp van verzoek/antwoord door correlatie-ID's toe te voegen.
  • Het is ideaal voor processen die beter werken wanneer ze over alle tabbladen worden samengevoegd, zoals multiplexen van WebSocket-verbindingen of het serialiseren van toegang tot IndexedDB.
  • Het verfijnen van details zoals bundlers, typedefinities en herverbinding kan het onderhoud en de gebruikerservaring aanzienlijk verbeteren.

Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.

YouTube Video