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 & 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 eenService Worker
.Service Worker
fungeert vooral als een netwerkproxy, terwijlShared 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
, eenMessagePort
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 standaardonclose
-handler. Stuur aan de hoofdkant een{type: 'bye'}
-bericht metport.postMessage
tijdensbeforeunload
, 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', enDedicated 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 vanport.addEventListener('message', ...)
moet uport.start()
aanroepen. Dit is niet nodig als uport.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 (zoalsArrayBuffer
) te verzenden of gedeeld geheugen (SharedArrayBuffer
) samen metAtomics
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.