Delt Worker i JavaScript

Delt Worker i JavaScript

Denne artikkelen forklarer Shared Worker i JavaScript.

Vi vil forklare alt fra grunnleggende om Shared Worker til praktiske brukstilfeller, trinn for trinn.

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>

Delt Worker i JavaScript

Hva er en Shared Worker?

En Shared Worker er en arbeidsprosess som kan deles mellom flere sider (faner, vinduer, iframes, osv.) innenfor samme opprinnelse. I motsetning til en side-spesifikk Dedicated Worker, er hovedfunksjonen at en enkelt bakgrunnsprosess kan deles mellom flere sider. Typiske brukstilfeller inkluderer følgende:.

  • Du kan dele én enkelt WebSocket-tilkobling mellom flere faner, noe som reduserer antall tilkoblinger og sentraliserer gjenkobling.
  • Du kan synkronisere tilstand mellom faner (sentralisert administrasjon for Pub/Sub eller lagring).
  • Du kan serialisere IndexedDB-operasjoner, og dermed håndtere samtidig tilgang.
  • Du kan forhindre duplisert kjøring av ressurskrevende prosesser.

Shared Worker har en annen rolle enn Service Worker. Service Worker fungerer hovedsakelig som en nettverksproxy, mens Shared Worker fokuserer på å utføre vilkårlige beregninger eller tilstandshåndtering delt mellom sider.

Grunnleggende API og livssyklus

Opprettelse (Hovedtråd)

 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 koden viser hvordan du oppretter en Shared Worker og sender/mottar meldinger gjennom dens port.

Mottaker (innenfor 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 koden viser hvordan en MessagePort mottas for hver tilkobling i Shared Worker, og hvordan meldinger fra klienter behandles og besvares.

Livssyklus for en Shared Worker

Shared Worker starter når den første tilkoblingen er etablert og kan avsluttes når den siste porten er lukket. Tilkoblinger lukkes ved omlasting/lukking av siden, og arbeideren blir gjenopprettet om nødvendig.

Shared Worker er et "langlevet skript delt i nettleseren". Hvis du ikke er oppmerksom på livssyklusen til Shared Worker, er det mer sannsynlig at problemer som ressurslekkasjer, foreldet tilstand og utilsiktede omstarter kan oppstå.

Prøv: Broadcast mellom faner

Dette er en minimal implementasjon der flere faner kobler seg til samme Shared Worker og alle sendte meldinger kringkastes 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 koden kobler til en Shared Worker, implementerer mottak/visning av meldinger, sending fra et skjema og sender en ping ved å klikke på en knapp.

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 koden implementerer enkel pub/sub-funksjonalitet inne i Shared Worker for å videresende meldinger mellom 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 åpner denne siden i flere faner, vil meldinger sendt fra en fane varsles til de andre.

Meldingsdesign: Forespørsel/svar og korrelasjons-ID

Når flere klienter samhandler med en Shared Worker, ønsker du ofte å vite hvilket svar som hører til hvilken forespørsel. Derfor er det standard praksis å inkludere en korrelasjons-ID for å knytte svar til forespørsler.

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 koden implementerer en enkel RPC-klient som kan utføre asynkrone metodekall til Shared Worker. Her refererer en RPC-server (Remote Procedure Call-server) til en server som tilbyr en mekanisme for å kalle funksjoner og prosedyrer fra andre programmer eller prosesser.
  • I dette eksemplet blir id rett og slett økt med én, men du kan også bruke en UUID, en tilfeldig streng, eller kombinere et tidsstempel med en teller for å lage en unik nøkkel.

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 koden implementerer en enkel RPC-server som utfører behandling basert på method og sender resultatet tilbake sammen med forespørsels-IDen.

Typiske bruksmønstre

Det finnes flere praktiske mønstre for bruk av Shared Worker, for eksempel:.

Multipleksing av én enkelt WebSocket (delt mellom faner)

Hvis hver fane åpner sin egen WebSocket-tilkobling, vil det påvirke serverbelastningen og tilkoblingsgrensen. Ha bare én WebSocket i Shared Worker, og la hver fane sende/motta meldinger via arbeidstråden.

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 koden implementerer prosess for sending/mottak av meldinger til en WebSocket-server og mottak av tilkoblingsstatusvarsler 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 koden implementerer en mekanisme inne i Shared Worker for å dele en enkelt WebSocket-tilkobling, kringkaste tilkoblingsstatus og mottatte data til alle klienter, samtidig som hver klients utgående forespørsler sendes til serveren via WebSocket.

  • Ved å sentralisere gjentilkoblingsstrategier og kø for ikke-sendte meldinger i Shared Worker, blir oppførselen konsekvent på tvers av alle faner.

IndexedDB-megling (serialisering)

Når du får tilgang til samme database fra flere faner og ønsket å unngå konflikter fra samtidige transaksjoner eller låseventing, kan du sette dem i kø i Shared Worker og behandle dem i serie.

 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 koden implementerer en mekanisme hvor lese- og skriveoperasjoner til IndexedDB settes i kø via Shared Worker, slik at tilgang fra flere faner serialiseres for å unngå konflikter og låse-problemer.

Modularisering og tips for bundlere

Hvis du vil skrive ditt Shared Worker-skript som et ES-modul, kan oppførsel og støtte variere mellom ulike miljøer. I praksis er det tryggere å velge ett av følgende:.

  • Skriv i klassisk worker-format, og bruk importScripts() for å laste inn avhengigheter om nødvendig.
  • Bruk worker entry-funksjonen i din bundler (for eksempel Vite / Webpack / esbuild osv.), og lag en egen pakke/bundle for Shared Worker under bygging.

Tips for feilhåndtering, diskonsekstdeteksjon og robusthet

Vurder følgende punkter for feilhåndtering og robusthet:.

  • Håndtering av sendinger før tilkoblingen er etablert Sett meldinger som kommer før porten er klar i kø.

  • Deteksjon av frakobling MessagePort har ikke en standard onclose-håndterer. På hovedsiden, send en {type: 'bye'}-melding med port.postMessage under beforeunload, eller angi tydelig i protokollen på annen måte for å sikre at arbeideren blir ryddet opp.

  • Gjenkobling Når en side lastes om eller en fane åpnes på nytt opprettes en ny port. Forbered en initial synkroniseringsmelding (for å sende hele tilstanden på en gang).

  • Backpressure Ved mye kringkasting, bytt til throttling/debouncing eller sending av øyeblikksbilder.

  • Sikkerhet En Shared Worker er fundamentalt delt innenfor samme opprinnelse. Hvis du legger hemmeligheter i workeren, bør du vurdere å lage sidesjekk/verifikasjon med tokens eller lignende på kallesiden.

Hvordan bruke Dedicated Worker, Shared Worker og Service Worker på riktig måte

Hver type Worker har følgende egenskaper:.

  • Dedicated Worker Dedicated Worker er ment for bruk av kun én enkelt side. Det muliggjør en 1:1-separasjon av beregninger fra brukergrensesnittet.

  • Shared Worker Shared Worker kan deles mellom flere sider med samme opprinnelse. Den er ideell for kommunikasjon mellom faner og deling av én enkelt tilkobling.

  • Service Worker Service Worker kan brukes for nettverksproxy, caching, offline-operasjoner, push-varsler og bakgrunnssynkronisering. Dens styrke er evnen til å avskjære fetch-forespørsler.

Som tommelfingerregel: bruk Shared Worker for 'informasjonsdeling og vilkårlig behandling mellom faner', Service Worker for 'nettverkskontroll', og Dedicated Worker hvis du bare vil avlaste tung prosessering fra brukergrensesnittet.

Vanlige fallgruver

Når du bruker en Shared Worker, må du være oppmerksom på følgende punkter.

  • Å glemme å kalle start() eller unødvendige kall Når du bruker port.addEventListener('message', ...), må du kalle port.start(). Dette er unødvendig hvis du bruker port.onmessage = ....

  • Ubegrenset kringkasting Belastningen øker ettersom antall faner øker. Du kan redusere belastningen ved å implementere differensiert levering eller abonnementsemner (filtrering etter emne).

  • Kopieringskostnad for objekter postMessage dupliserer dataene. For store datamengder, vurder å sende det som Transferable (for eksempel ArrayBuffer) eller bruk delt minne (SharedArrayBuffer) sammen med Atomics.

  • Levetid og re-initiering Shared Worker kan avsluttes når den siste klienten kobler fra. Design initialiseringen riktig for første tilkoblinger og omstarter for å unngå bivirkninger og feil.

Sammendrag

  • Shared Worker er et sted for å implementere langvarig, tilpasset logikk delt mellom flere sider.
  • Klargjør meldingskontrakten (typer/protokoller) og gjør forespørsels-/svar-designet robust ved å legge til korrelasjons-IDer.
  • Den er ideell for prosesser som fungerer bedre når de er samlet for alle faner, slik som multipleksing av WebSocket-tilkoblinger eller seriel tilgang til IndexedDB.
  • Finjustering av detaljer som bundlere, typedefinisjoner og gjenkobling kan i stor grad forbedre vedlikeholdbarhet og brukeropplevelse.

Du kan følge med på artikkelen ovenfor ved å bruke Visual Studio Code på vår YouTube-kanal. Vennligst sjekk ut YouTube-kanalen.

YouTube Video