Shared Worker i JavaScript

Shared Worker i JavaScript

Denne artikel forklarer Shared Worker i JavaScript.

Vi vil forklare alt fra det grundlæggende ved Shared Worker til praktiske brugsecases trin for trin.

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 i JavaScript

Hvad er en Shared Worker?

En Shared Worker er en arbejdståd, der kan deles mellem flere sider (faner, vinduer, iframes osv.) inden for samme oprindelse. I modsætning til en side-specifik Dedicated Worker er hovedfunktionen, at en enkelt baggrundsproces kan deles mellem flere sider. Typiske anvendelsestilfælde inkluderer følgende:.

  • Du kan dele en enkelt WebSocket-forbindelse på tværs af flere faner, hvilket reducerer antallet af forbindelser og centraliserer genopkoblinger.
  • Du kan synkronisere tilstand mellem faner (centraliseret styring af Pub/Sub eller data-lagre).
  • Du kan serialisere IndexedDB-operationer og styre samtidig adgang.
  • Du kan forhindre dobbelt udførsel af beregningstunge processer.

Shared Worker har en anden rolle end Service Worker. Service Worker fungerer primært som en netværksproxy, mens Shared Worker fokuserer på at udføre vilkårlige beregninger eller tilstandsstyring, der deles mellem sider.

Grundlæggende API og livscyklus

Oprettelse (hovedtråden)

 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 });
  • Denne kode viser, hvordan man opretter en Shared Worker og sender/modtager beskeder gennem dens port.

Modtager (inden for Shared Worker-området)

 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)
  • Denne kode demonstrerer, hvordan en MessagePort modtages for hver forbindelse i Shared Worker, og behandler samt svarer på beskeder fra klienter.

Livscyklus for en Shared Worker

Shared Worker starter, når den første forbindelse etableres og kan afslutte, når den sidste port lukkes. Forbindelser lukkes, når siden genindlæses eller lukkes, og worker'en genskabes om nødvendigt.

Shared Worker er et "langvarigt script, der deles i browseren". Hvis du ikke er opmærksom på Shared Worker-livscyklussen, er der større sandsynlighed for problemer som ressource-lækager, forældet tilstand og utilsigtede genstarter.

Prøv det: Broadcast mellem faner

Dette er en minimal implementering, hvor flere faner forbinder til den samme Shared Worker, og enhver besked sendes videre til alle faner.

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});
  • Denne kode forbinder til en Shared Worker, implementerer modtagelse/visning af beskeder, afsendelse via en formular, og sender et ping ved knaptryk.

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};
  • Denne kode implementerer en simpel pub/sub-funktion i Shared Worker til videresendelse af beskeder mellem flere klienter.

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>
  • Hvis du åbner denne side i flere faner, vil beskeder sendt fra en hvilken som helst fane blive vist til de andre.

Beskeddtekning: Request/Response og Correlation ID

Når flere klienter interagerer med en Shared Worker, ønsker du ofte at identificere, hvilket svar der hører til hvilken forespørgsel. Derfor er det standardpraksis at inkludere et korrelations-ID for at knytte svar til forespørgsler.

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'
  • Denne kode implementerer en simpel RPC-klient, som kan lave asynkrone metodekald til Shared Worker. Her henviser en RPC-server (Remote Procedure Call-server) til en server, der giver en mekanisme til at kalde funktioner og procedurer fra andre programmer eller processer.
  • I dette eksempel øges id blot, men du kan også bruge en UUID, en tilfældig streng eller kombinere et tidsstempel med en tæller for at lave en unik nøgle.

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};
  • Denne kode implementerer en enkel RPC-server, der udfører behandling i henhold til method og sender resultatet tilbage sammen med anmodnings-ID'et.

Typiske anvendelsesmønstre

Der er flere praktiske anvendelsesmønstre for Shared Worker, såsom følgende:.

Multiplexing af en enkelt WebSocket (delt mellem faner)

Hvis hver fane åbner sin egen WebSocket-forbindelse, vil det påvirke serverbelastningen og forbindelsesgrænsen. Placer kun én WebSocket i Shared Worker, og lad hver fane sende/modtage beskeder via worker'en.

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}
  • Denne kode implementerer processen med at sende/modtage beskeder til en WebSocket-server og modtage forbindelsesstatus via en 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};
  • Denne kode implementerer et system i Shared Worker til at dele én WebSocket-forbindelse, udsende forbindelsesstatus og modtaget data til alle klienter, mens hver klients udgående forespørgsler sendes via WebSocket til serveren.

  • Ved at centralisere genopkoblingsstrategier og kø til ikke-afsendte beskeder i Shared Worker bliver adfærden konsekvent på tværs af alle faner.

IndexedDB-mægling (serialisering)

Når du tilgår den samme database fra flere faner, og vil undgå konflikter fra samtidige transaktioner eller ventetid på låse, kan du sætte dem i kø i Shared Worker og behandle dem sekventielt.

 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};
  • Denne kode implementerer et system, hvor læse-/skriveoperationer til IndexedDB sættes i kø via Shared Worker, så adgang fra flere faner serialiseres for at undgå konflikter og låser.

Modularisering og bundlertips

Hvis du ønsker at skrive dit Shared Worker-script som et ES-modul, kan adfærd og understøttelse variere afhængigt af miljøet. I praksis er det sikrere at vælge en af følgende:.

  • Skriv i klassisk worker-format, og brug importScripts() til at indlæse afhængigheder om nødvendigt.
  • Brug bundlerens worker-indgangspunkt (som Vite / Webpack / esbuild osv.), og opret et separat bundle til Shared Worker under bygning.

Tips til fejlhåndtering, detektion af afbrydelse og robusthed

Overvej følgende punkter for fejlhåndtering og robusthed:.

  • Håndtering af afsendelse før forbindelse er oprettet Sæt beskeder, der ankommer før porten er klar, i kø.

  • Detektion af afbrydelse MessagePort har ikke en standard onclose-håndtering. På hovedsiden skal du sende en {type: 'bye'}-besked med port.postMessage under beforeunload, eller på anden vis angive det tydeligt i protokollen for at sikre, at worker'en bliver ryddet op.

  • Genopkobling Når en side genindlæses eller en fane genåbnes, oprettes en ny port. Forbered en initial synkroniseringsbesked (til at sende hele tilstanden på én gang).

  • Backpressure Ved tung broadcasting, skift til throttling/debouncing eller send snapshots.

  • Sikkerhed En Shared Worker deles grundlæggende inden for samme oprindelse. Hvis du placerer hemmeligheder i worker'en, bør du overveje at designe sideskuds-bekræftelse med tokens eller lignende midler på kaldesiden.

Sådan bruges Dedicated Worker, Shared Worker og Service Worker korrekt

Hver type Worker har følgende egenskaber:.

  • Dedicated Worker Dedicated Worker er kun beregnet til brug af én enkelt side. Det muliggør en 1:1-adskillelse af beregninger fra brugergrænsefladen.

  • Shared Worker Shared Worker kan deles mellem flere sider med samme oprindelse. Det er ideelt til kommunikation mellem faner og deling af én forbindelse.

  • Service Worker Service Worker kan bruges til netværksproxy, caching, offlinefunktioner, push-beskeder og baggrundssynkronisering. Dens styrke er evnen til at opsnappe fetch-anmodninger.

Som tommelfingerregel: brug Shared Worker til 'informationsdeling og vilkårlig behandling mellem faner', Service Worker til 'netværkskontrol', og Dedicated Worker, hvis du blot vil aflaste tunge processer fra brugerfladen.

Almindelige faldgruber

Når du bruger en Shared Worker, skal du være opmærksom på følgende punkter.

  • At glemme at kalde start() eller unødvendige kald Når du bruger port.addEventListener('message', ...), skal du kalde port.start(). Dette er unødvendigt, hvis du bruger port.onmessage = ....

  • Ubegrænset broadcasting Belastningen stiger, efterhånden som antallet af faner øges. Du kan reducere belastningen ved at implementere differentieret levering eller abonnementsemner (filtrering efter emne).

  • Kopieringsomkostninger for objekter postMessage duplikerer dataene. Ved store datamængder bør du overveje at sende det som Transferable (f.eks. ArrayBuffer) eller ved at bruge delt hukommelse (SharedArrayBuffer) sammen med Atomics.

  • Levetid og re-initialisering Shared Worker kan afsluttes, når den sidste klient afbryder forbindelsen. Design korrekt initialisering til første forbindelser og genstarter for at undgå bivirkninger og fejl.

Sammendrag

  • Shared Worker er et sted til at implementere langvarig brugerdefineret logik, der deles mellem flere sider.
  • Uddyb beskedkontrakten (typer/protokoller) og gør dit anmodnings/svar-design robust ved at tilføje korrelations-IDs.
  • Det er ideelt til processer, der fungerer bedre, når de er samlet på tværs af alle faner, såsom multiplex af WebSocket-forbindelser eller serialisering af adgang til IndexedDB.
  • Finjustering af detaljer som bundlere, typedefinitioner og genforbindelse kan forbedre vedligeholdelse og brugeroplevelse markant.

Du kan følge med i ovenstående artikel ved hjælp af Visual Studio Code på vores YouTube-kanal. Husk også at tjekke YouTube-kanalen.

YouTube Video