Shared Worker in JavaScript

Shared Worker in JavaScript

Dieser Artikel erklärt den Shared Worker in JavaScript.

Wir erklären alles vom Grundlegenden des Shared Worker bis hin zu praktischen Anwendungsfällen Schritt für Schritt.

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

Was ist ein Shared Worker?

Ein Shared Worker ist ein Worker-Thread, der zwischen mehreren Seiten (Tabs, Fenstern, iframes usw.) innerhalb derselben Origin geteilt werden kann. Im Gegensatz zu einem seiten-spezifischen Dedicated Worker besteht das Hauptmerkmal darin, dass ein einziger Hintergrundprozess von mehreren Seiten gemeinsam genutzt werden kann. Typische Anwendungsfälle sind unter anderem:.

  • Sie können eine einzelne WebSocket-Verbindung zwischen mehreren Tabs teilen, wodurch die Anzahl der Verbindungen reduziert und das erneute Verbinden zentralisiert wird.
  • Sie können den Status zwischen Tabs synchronisieren (zentrale Verwaltung für Pub/Sub oder Stores).
  • Sie können IndexedDB-Operationen serialisieren und gleichzeitigen Zugriff vermitteln.
  • Sie können die doppelte Ausführung rechenintensiver Prozesse verhindern.

Shared Worker hat eine andere Rolle als Service Worker. Service Worker fungiert hauptsächlich als Netzwerk-Proxy, während sich der Shared Worker darauf konzentriert, beliebige Berechnungen oder Zustandsverwaltung seitenübergreifend auszuführen.

Grundlegende API und Lebenszyklus

Erstellung (Hauptthread)

 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 });
  • Dieser Code zeigt, wie ein Shared Worker erstellt und wie Nachrichten über dessen Port gesendet/empfangen werden.

Empfänger (im Gültigkeitsbereich des 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)
  • Dieser Code zeigt, wie im Shared Worker für jede Verbindung ein MessagePort empfangen wird und Nachrichten von Clients verarbeitet und beantwortet werden.

Lebenszyklus eines Shared Worker

Shared Worker startet, wenn die erste Verbindung hergestellt wird, und kann beendet werden, wenn der letzte Port geschlossen wird. Verbindungen werden beim Neuladen oder Schließen der Seite geschlossen, und der Worker wird bei Bedarf neu erstellt.

Shared Worker ist ein „lang laufendes Skript, das im Browser gemeinsam genutzt wird“. Wenn Sie den Lebenszyklus des Shared Worker nicht beachten, können Probleme wie Ressourcenlecks, veralteter Zustand und unbeabsichtigte Neustarts häufiger auftreten.

Probieren Sie es aus: Broadcast zwischen Tabs

Dies ist eine Minimalimplementierung, bei der mehrere Tabs denselben Shared Worker nutzen und jede gesendete Nachricht an alle Tabs übertragen wird.

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});
  • Dieser Code verbindet sich mit einem Shared Worker, implementiert den Nachrichteneingang/-anzeige, Senden über ein Formular sowie das Senden eines Pings durch Klicken auf einen Button.

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};
  • Dieser Code implementiert eine einfache Pub/Sub-Funktion im Shared Worker, um Nachrichten zwischen mehreren Clients weiterzuleiten.

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>
  • Wenn Sie diese Seite in mehreren Tabs öffnen, werden Nachrichten, die von einem Tab gesendet werden, den anderen angezeigt.

Nachrichtendesign: Request/Response und Korrelations-ID

Wenn mehrere Clients mit einem Shared Worker interagieren, möchten Sie häufig wissen, welche Antwort zu welcher Anfrage gehört. Daher ist es gängige Praxis, eine Korrelations-ID hinzuzufügen, um Antworten mit Anfragen zu verknüpfen.

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'
  • Dieser Code implementiert einen einfachen RPC-Client, der asynchrone Methodenaufrufe an den Shared Worker senden kann. Hier bezeichnet ein RPC-Server (Remote Procedure Call Server) einen Server, der einen Mechanismus bereitstellt, um Funktionen und Prozeduren aus anderen Programmen oder Prozessen aufzurufen.
  • In diesem Beispiel wird die id einfach inkrementiert, aber man könnte auch eine UUID, eine zufällige Zeichenkette oder eine Kombination aus Zeitstempel und Zähler verwenden, um einen eindeutigen Schlüssel zu erzeugen.

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};
  • Dieser Code implementiert einen einfachen RPC-Server, der die Verarbeitung entsprechend der method ausführt und das Ergebnis zusammen mit der Anfrage-ID zurücksendet.

Typische Nutzungsmuster

Es gibt mehrere praktische Muster für die Nutzung von Shared Worker, z. B.:.

Multiplexing einer einzigen WebSocket-Verbindung (geteilt über mehrere Tabs)

Wenn jeder Tab seine eigene WebSocket-Verbindung öffnet, wirkt sich das auf die Serverlast und das Verbindungslimit aus. Platzieren Sie nur eine WebSocket im Shared Worker, und jeder Tab sendet/empfängt Nachrichten über den 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}
  • Dieser Code implementiert das Senden/Empfangen von Nachrichten an/zu einem WebSocket-Server sowie das Empfangen von Verbindungsstatus-Benachrichtigungen via 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};
  • Dieser Code implementiert im Shared Worker einen Mechanismus, um eine einzelne WebSocket-Verbindung zu teilen, Verbindungsstatus und empfangene Daten an alle Clients zu senden und die ausgehenden Anfragen jedes Clients über die WebSocket an den Server zu leiten.

  • Durch die Zentralisierung von Wiederverbindungsstrategien und nicht gesendeten Nachrichtenwarteschlangen im Shared Worker wird das Verhalten in allen Tabs vereinheitlicht.

IndexedDB-Vermittlung (Serialisierung)

Wenn Sie von mehreren Tabs aus auf dieselbe Datenbank zugreifen und Konflikte durch gleichzeitige Transaktionen oder Lock-Warteschlangen vermeiden möchten, können Sie die Anfragen im Shared Worker sammeln und sie nacheinander abarbeiten.

 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};
  • Dieser Code implementiert einen Mechanismus, bei dem Lese-/Schreiboperationen an IndexedDB über den Shared Worker in eine Warteschlange gestellt werden, sodass der Zugriff von mehreren Tabs serialisiert wird, um Konflikte und Sperrkonkurrenz zu vermeiden.

Modularisierung und Bundler-Tipps

Wenn Sie Ihr Shared Worker-Skript als ES-Modul schreiben möchten, können Verhalten und Unterstützung je nach Umgebung unterschiedlich sein. In der Praxis ist es sicherer, eine der folgenden Möglichkeiten zu wählen:.

  • Schreiben Sie im klassischen Worker-Format und nutzen Sie importScripts(), um bei Bedarf Abhängigkeiten zu laden.
  • Verwenden Sie das Worker-Entry-Feature Ihres Bundlers (z. B. Vite / Webpack / esbuild etc.) und erstellen Sie beim Build einen separaten Bundle für den Shared Worker.

Tipps für Fehlerbehandlung, Erkennung von Verbindungsabbrüchen und Robustheit

Beachten Sie die folgenden Punkte für Fehlerbehandlung und Robustheit:.

  • Umgang mit Send-Anforderungen vor Verbindungsaufbau Stellen Sie Nachrichten, die ankommen, bevor der Port bereit ist, in eine Warteschlange.

  • Erkennung von Verbindungsabbrüchen MessagePort hat keinen standardisierten onclose-Handler. Auf der Hauptseite sollte während beforeunload eine Nachricht {type: 'bye'} mit port.postMessage gesendet werden oder anderweitig im Protokoll klar angegeben werden, um sicherzustellen, dass der Worker aufgeräumt wird.

  • Wiederverbindung Wenn eine Seite neu geladen oder ein Tab wieder geöffnet wird, wird ein neuer Port erstellt. Bereiten Sie eine anfängliche Synchronisationsnachricht vor (um den vollständigen Status auf einmal zu senden).

  • Backpressure Bei starker Broadcast-Nutzung wechseln Sie zu Throttling/Debouncing oder dem Versenden von Snapshots.

  • Sicherheit Ein Shared Worker wird grundsätzlich innerhalb derselben Origin geteilt. Wenn Sie Geheimnisse im Worker speichern, sollten Sie eine seitliche Verifikation mit Tokens oder ähnlichen Mechanismen auf der aufrufenden Seite vorsehen.

Wie man Dedicated Worker, Shared Worker und Service Worker angemessen verwendet

Jeder Worker-Typ hat die folgenden Eigenschaften:.

  • Dedicated Worker Dedicated Worker ist ausschließlich für die Nutzung durch eine einzelne Seite vorgesehen. Er ermöglicht eine 1:1-Trennung der Berechnungen von der Benutzeroberfläche.

  • Shared Worker Shared Worker kann von mehreren Seiten mit demselben Ursprung geteilt werden. Er ist ideal für die Kommunikation zwischen Tabs und das Teilen einer einzigen Verbindung.

  • Service Worker Service Worker kann für Netzwerk-Proxying, Caching, Offline-Betrieb, Push-Benachrichtigungen und Hintergrundsynchronisation verwendet werden. Seine Stärke liegt in der Fähigkeit, Fetch-Anfragen abzufangen.

Als Faustregel gilt: Verwenden Sie Shared Worker für 'Informationsaustausch und beliebige Verarbeitung zwischen Tabs', Service Worker für 'Netzwerksteuerung' und Dedicated Worker, wenn Sie nur schwere Berechnungen vom UI auslagern möchten.

Häufige Fallstricke

Beim Einsatz eines Shared Worker sollten Sie auf folgende Punkte achten.

  • Vergessen, start() aufzurufen oder unnötige Aufrufe Wenn Sie port.addEventListener('message', ...) verwenden, müssen Sie port.start() aufrufen. Dies ist nicht erforderlich, wenn Sie port.onmessage = ... verwenden.

  • Unkontrolliertes Broadcasting Die Last steigt an, je mehr Tabs geöffnet sind. Sie können die Last verringern, indem Sie differenzierte Zustellung oder Abonnement-Themen (Themenfilter) implementieren.

  • Kopieraufwand für Objekte postMessage dupliziert die Daten. Für große Datenmengen sollten Sie diese als Transferable (wie etwa ArrayBuffer) versenden oder gemeinsamen Speicher (SharedArrayBuffer) in Kombination mit Atomics verwenden.

  • Lebensdauer und Re-Initialisierung Shared Worker kann beendet werden, wenn der letzte Client die Verbindung trennt. Entwerfen Sie die Initialisierung für erste Verbindungen und Neustarts sorgfältig, um Nebenwirkungen und Fehler zu vermeiden.

Zusammenfassung

  • Shared Worker ist ein Ort, um langlebige, benutzerdefinierte Logik, die über mehrere Seiten geteilt wird, zu implementieren.
  • Klären Sie den Nachrichtenvertrag (Typen/Protokolle) und machen Sie das Request/Response-Design robust, indem Sie Korrelations-IDs anhängen.
  • Er ist ideal für Prozesse, die über alle Tabs hinweg vereinheitlicht besser funktionieren, wie z.B. Multiplexing von WebSocket-Verbindungen oder Serialisierung des Zugriffs auf IndexedDB.
  • Feinabstimmung von Details wie Bundlern, Typdefinitionen und Wiederverbindung kann die Wartbarkeit und Benutzererfahrung erheblich verbessern.

Sie können den obigen Artikel mit Visual Studio Code auf unserem YouTube-Kanal verfolgen. Bitte schauen Sie sich auch den YouTube-Kanal an.

YouTube Video