Współdzielony Worker w JavaScript

Współdzielony Worker w JavaScript

Ten artykuł wyjaśnia Shared Worker w JavaScript.

Wyjaśnimy wszystko, od podstaw Shared Worker po praktyczne przypadki użycia, krok po kroku.

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>

Współdzielony Worker w JavaScript

Czym jest Shared Worker?

Shared Worker to wątek worker, który może być współdzielony pomiędzy wieloma stronami (kartami, oknami, iframe itp.) w tym samym pochodzeniu (origin). W przeciwieństwie do przypisanego do konkretnej strony Dedicated Worker, główną cechą jest to, że jeden proces w tle może być współdzielony przez wiele stron. Typowe przypadki użycia obejmują następujące sytuacje:.

  • Możesz współdzielić jedno połączenie WebSocket pomiędzy wieloma kartami, co zmniejsza liczbę połączeń i centralizuje ponowne nawiązywanie połączeń.
  • Możesz synchronizować stan pomiędzy kartami (scentralizowane zarządzanie dla Pub/Sub lub stores).
  • Możesz serializować operacje na IndexedDB, pośrednicząc w jednoczesnym dostępie.
  • Możesz zapobiec wielokrotnemu uruchamianiu kosztownych obliczeniowo procesów.

Shared Worker pełni inną rolę niż Service Worker. Service Worker działa głównie jako proxy sieciowe, podczas gdy Shared Worker skupia się na wykonywaniu dowolnych obliczeń lub zarządzaniu stanem współdzielonym pomiędzy stronami.

Podstawowy interfejs API i cykl życia

Tworzenie (główny wątek)

 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 });
  • Ten kod pokazuje, jak utworzyć Shared Worker i wysyłać/odbierać wiadomości przez jego port.

Odbiornik (w zakresie 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)
  • Ten kod pokazuje, jak w Shared Worker dla każdego połączenia odbierany jest MessagePort oraz jak przetwarzać i odpowiadać na wiadomości od klientów.

Cykl życia Shared Worker

Shared Worker rozpoczyna działanie, gdy zostanie nawiązane pierwsze połączenie i może zostać zakończony, gdy ostatni port zostanie zamknięty. Połączenia są zamykane po przeładowaniu/zamknięciu strony, a worker jest tworzony na nowo w razie potrzeby.

Shared Worker to "długotrwały skrypt współdzielony w przeglądarce". Jeśli nie będziesz zwracać uwagi na cykl życia Shared Workera, mogą wystąpić problemy takie jak wycieki zasobów, utrzymywanie się nieaktualnego stanu oraz niezamierzone ponowne uruchomienia.

Wypróbuj: Broadcast pomiędzy kartami

Jest to minimalna implementacja, w której wiele kart łączy się z tym samym Shared Worker i każda wysłana wiadomość jest przesyłana do wszystkich kart.

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});
  • Ten kod łączy się z Shared Worker, implementuje odbiór/wyświetlanie wiadomości, wysyłanie z formularza oraz wysyłanie wiadomości ping poprzez kliknięcie przycisku.

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};
  • Ten kod implementuje prostą funkcję pub/sub wewnątrz Shared Worker, aby przekazywać wiadomości między wieloma klientami.

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>
  • Jeśli otworzysz tę stronę w wielu kartach, wysłane wiadomości zostaną przekazane do pozostałych.

Projektowanie wiadomości: Request/Response oraz Correlation ID

Gdy wiele klientów korzysta z Shared Worker, często chcesz zidentyfikować która odpowiedź należy do którego zapytania. Dlatego standardową praktyką jest dodawanie identyfikatora powiązania (correlation ID), aby powiązać odpowiedzi z zapytaniami.

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'
  • Ten kod implementuje prostego klienta RPC, który umożliwia asynchroniczne wywoływanie metod w Shared Worker. Tutaj serwer RPC (Remote Procedure Call) odnosi się do serwera umożliwiającego wywoływanie funkcji i procedur z innych programów lub procesów.
  • W tym przykładzie id jest po prostu inkrementowane, ale możesz też użyć UUID, losowego ciągu znaków lub połączyć znacznik czasu z licznikiem, aby utworzyć unikalny klucz.

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};
  • Ten kod implementuje prosty serwer RPC, który wykonuje operację zgodnie z podaną method, a następnie odsyła wynik wraz z identyfikatorem zapytania.

Typowe wzorce użycia

Istnieje kilka praktycznych wzorców użycia Shared Worker, między innymi:.

Multipleksowanie pojedynczego WebSocket (współdzielonego pomiędzy kartami)

Jeśli każda karta otwiera własne połączenie WebSocket, wpłynie to na obciążenie serwera i limit połączeń. Umieść tylko jeden WebSocket w Shared Worker, a każda karta wysyła/odbiera wiadomości przez 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}
  • Ten kod implementuje wysyłanie/odbieranie wiadomości do serwera WebSocket oraz odbieranie powiadomień o stanie połączenia poprzez 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};
  • Ten kod implementuje mechanizm współdzielenia pojedynczego połączenia WebSocket w Shared Worker, rozsyłając status połączenia i otrzymane dane do wszystkich klientów oraz wysyłając żądania klientów do serwera przez WebSocket.

  • Centralizując strategie ponownego łączenia i kolejkowanie nieprzesłanych wiadomości w Shared Worker, zachowanie staje się spójne we wszystkich kartach.

Pośredniczenie na IndexedDB (serializacja)

Podczas dostępu do tej samej bazy danych z wielu kart, aby uniknąć konfliktów jednoczesnych transakcji albo oczekiwania na odblokowanie, możesz kolejkować operacje w Shared Worker i realizować je sekwencyjnie.

 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};
  • Ten kod implementuje mechanizm, w którym operacje odczytu/zapisu do IndexedDB są kolejkowane przez Shared Worker, serializując dostęp z wielu kart w celu uniknięcia konfliktów i oczekiwania na zwolnienie blokady.

Modularyzacja i wskazówki do bundlerów

Jeśli chcesz napisać swój skrypt Shared Worker jako moduł ES, zachowanie i wsparcie mogą się różnić w zależności od środowiska. W praktyce bezpieczniej jest wybrać jedno z poniższych:.

  • Napisz w klasycznym formacie worker i użyj importScripts(), aby załadować zależności w razie potrzeby.
  • Użyj funkcji wejścia workera w swoim bundlerze (np. Vite / Webpack / esbuild itp.) i utwórz osobny bundle dla Shared Worker podczas budowania.

Wskazówki dotyczące obsługi błędów, wykrywania rozłączeń i odporności

Zwróć uwagę na następujące punkty dotyczące obsługi błędów i odporności:.

  • Obsługa wysyłek przed nawiązaniem połączenia Kolejkuj wiadomości, które pojawią się zanim port będzie gotowy.

  • Wykrywanie rozłączenia MessagePort nie posiada standardowego handlera onclose. Po stronie głównej wyślij wiadomość {type: 'bye'} za pomocą port.postMessage podczas beforeunload lub wyraźnie określ to w protokole, aby upewnić się, że worker zostanie zakończony.

  • Ponowne połączenie Po przeładowaniu strony lub ponownym otwarciu karty tworzony jest nowy port. Przygotuj wiadomość synchronizacji początkowej (aby wysłać pełny stan za jednym razem).

  • Presja zwrotna (Backpressure) Podczas intensywnej transmisji przestaw się na throttling/debouncing lub przesyłanie snapshotów.

  • Bezpieczeństwo Shared Worker jest zasadniczo współdzielony w ramach tego samego originu. Jeśli umieszczasz sekrety wewnątrz workera, rozważ zaprojektowanie weryfikacji po stronie wywołującej za pomocą tokenów lub podobnych rozwiązań.

Jak właściwie używać Dedicated Worker, Shared Worker i Service Worker

Każdy rodzaj Workera ma następujące cechy:.

  • Dedicated Worker Dedicated Worker jest przeznaczony do użycia tylko przez jedną stronę. Umożliwia 1:1 rozdział obliczeń od interfejsu użytkownika.

  • Shared Worker Shared Worker może być współdzielony przez wiele stron o tym samym pochodzeniu. Jest idealny do komunikacji między kartami oraz współdzielenia pojedynczego połączenia.

  • Service Worker Service Worker może być używany do proxy sieciowego, cache’owania, operacji offline, powiadomień push i synchronizacji w tle. Jego mocną stroną jest możliwość przechwytywania żądań fetch.

Jako ogólną zasadę: używaj Shared Worker do "współdzielenia informacji i dowolnego przetwarzania między kartami", Service Worker do "kontroli sieci", a Dedicated Worker jeśli chcesz tylko odciążyć ciężkie obliczenia od interfejsu użytkownika.

Typowe pułapki

Korzystając z Shared Worker, należy zwrócić uwagę na następujące kwestie.

  • Zapomnienie o wywołaniu start() lub niepotrzebne wywołania Gdy używasz port.addEventListener('message', ...), musisz wywołać port.start(). Jest to niepotrzebne, jeśli używasz port.onmessage = ....

  • Nieograniczone rozgłaszanie Obciążenie wzrasta wraz z liczbą otwartych kart. Można zmniejszyć obciążenie, wdrażając dostarczanie różnicowe lub tematy subskrypcji (filtrowanie po temacie).

  • Koszt kopiowania obiektów postMessage duplikuje dane. Dla dużych danych rozważ wysyłkę jako Transferable (np. ArrayBuffer) lub użycie pamięci współdzielonej (SharedArrayBuffer) razem z Atomics.

  • Czas życia i ponowna inicjalizacja Shared Worker może się zatrzymać, gdy ostatni klient się odłączy. Odpowiednio zaprojektuj inicjalizację dla pierwszych połączeń i restartów, aby uniknąć skutków ubocznych i błędów.

Podsumowanie

  • Shared Worker służy do implementowania długo żyjącej logiki współdzielonej między wieloma stronami.
  • Upewnij się, że kontrakt wiadomości (typy/protokoły) jest jasny i wzmacniaj projekt zapytań/odpowiedzi przez dodanie correlation ID.
  • Jest idealny do procesów, które działają lepiej w jednolitej formie we wszystkich kartach, takich jak multipleksowanie połączeń WebSocket czy serializowanie dostępu do IndexedDB.
  • Dopracowanie szczegółów takich jak bundlery, definicje typów i ponowne łączenie może znacznie poprawić łatwość utrzymania i jakość doświadczenia użytkownika.

Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.

YouTube Video