Общий работник (Shared Worker) в JavaScript

Общий работник (Shared Worker) в JavaScript

В этой статье объясняется, что такое Shared Worker в JavaScript.

Мы поэтапно рассмотрим всё, от основ Shared Worker до практических примеров использования.

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) в JavaScript

Что такое Shared Worker?

Shared Worker — это поток-работник, который может быть общим для нескольких страниц (вкладок, окон, iframe и т. д.) внутри одного origin. В отличие от специфичного для страницы Dedicated Worker, основная особенность заключается в том, что один фоновый процесс может быть разделен между несколькими страницами. Типичные варианты использования включают следующее:.

  • Можно разделять одно WebSocket-соединение между несколькими вкладками, уменьшая их количество и централизуя повторные подключения.
  • Можно синхронизировать состояние между вкладками (централизованное управление для Pub/Sub или хранилищ данных).
  • Можно сериализовать операции с IndexedDB, регулируя одновременный доступ.
  • Можно избежать повторного выполнения ресурсоёмких процессов.

Shared Worker выполняет другую роль, чем Service Worker. Service Worker в основном действует как сетевой прокси, а Shared Worker предназначен для выполнения различных вычислений или управления состоянием, совместно используемым между страницами.

Базовые API и жизненный цикл

Создание (главный поток)

 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 });
  • Этот код показывает, как создать Shared Worker и передавать/принимать сообщения через его порт.

Приёмник (в области действия 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)
  • Этот код показывает, как в Shared Worker для каждого подключения принимается MessagePort, обрабатываются и отправляются ответы на сообщения клиентов.

Жизненный цикл Shared Worker

Shared Worker запускается при установлении первого соединения и может завершиться при закрытии последнего порта. Соединения закрываются при перезагрузке/закрытии страницы, а работник при необходимости создаётся заново.

Shared Worker — это «долго живущий скрипт, используемый совместно внутри браузера». Если не учитывать жизненный цикл Shared Worker, с большей вероятностью возникнут такие проблемы, как утечки ресурсов, сохранение устаревшего состояния и непреднамеренные перезапуски.

Попробуйте: рассылка сообщений между вкладками

Это минимальная реализация, в которой несколько вкладок подключаются к одному Shared Worker, и любое отправленное сообщение доставляется всем вкладкам.

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});
  • Этот код подключается к Shared Worker, реализует приём/отображение сообщений, отправку из формы и отправку ping по нажатию кнопки.

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};
  • Этот код реализует внутри Shared Worker простую функцию pub/sub для передачи сообщений между несколькими клиентами.

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>
  • Если вы откроете эту страницу в нескольких вкладках, сообщения, отправленные из любой из них, будут видны остальным.

Дизайн сообщений: запрос/ответ и идентификатор корреляции (Correlation ID)

Когда несколько клиентов взаимодействуют с Shared Worker, часто требуется определить, какой ответ относится к какому запросу. Поэтому стандартной практикой является включение корреляционного идентификатора для связывания ответов с запросами.

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'
  • Этот код реализует простой RPC-клиент для асинхронных вызовов методов в Shared Worker. Здесь RPC-сервер (сервер удалённого вызова процедур) — это сервер, предоставляющий механизм для вызова функций и процедур из других программ или процессов.
  • В этом примере id просто увеличивается, но вы также можете использовать UUID, случайную строку или сочетать временную метку с счетчиком для создания уникального ключа.

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};
  • Этот код реализует простой RPC-сервер, который выполняет обработку в соответствии с method и отправляет результат обратно вместе с идентификатором запроса.

Типовые сценарии использования

Есть несколько практических схем применения Shared Worker, например:.

Мультиплексирование одного WebSocket (общий для всех вкладок)

Если каждая вкладка открывает собственное WebSocket-соединение, это повлияет на нагрузку на сервер и лимит подключений. Разместите только одно WebSocket-соединение в Shared 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}
  • Этот код реализует отправку/приём сообщений на WebSocket-сервер и получение уведомлений о состоянии соединения через 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};
  • Этот код реализует механизм внутри Shared Worker для совместного использования одного WebSocket-соединения, рассылки статусов соединения и полученных данных всем клиентам, а также отправки их индивидуальных запросов серверу через WebSocket.

  • Централизуя стратегии переподключения и очередь неотправленных сообщений в Shared Worker, поведение становится единообразным во всех вкладках.

Посредничество в работе с IndexedDB (сериализация запросов)

Когда доступ к одной базе данных осуществляется из нескольких вкладок, чтобы избежать конфликтов при одновременных транзакциях или ожидания разблокировки, можно ставить запросы в очередь через Shared Worker и обрабатывать их последовательно.

 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};
  • Этот код реализует механизм, при котором операции чтения/записи в IndexedDB ставятся в очередь через Shared Worker, сериализуя доступ из нескольких вкладок для предотвращения конфликтов и блокировок.

Советы по модульности и сборке (Bundler)

Если вы хотите написать скрипт Shared Worker как ES Module, поведение и поддержка зависят от среды исполнения. На практике безопаснее выбрать один из следующих подходов:.

  • Писать в классическом формате worker и загрузить зависимости через importScripts() при необходимости.
  • Использовать входную точку worker в вашем бандлере (например, Vite / Webpack / esbuild и др.) и создавать отдельный bundle для Shared Worker во время сборки.

Советы по обработке ошибок, обнаружению разрывов связи и устойчивости

Учитывайте следующие моменты при обработке ошибок и обеспечении устойчивости:.

  • Обработка отправки до установления подключения Помещайте в очередь сообщения, пришедшие до готовности порта.

  • Обнаружение разрыва соединения У MessagePort нет стандартного обработчика события onclose. На основной стороне отправляйте сообщение {type: 'bye'} с помощью port.postMessage во время beforeunload, или чётко определите это в протоколе, чтобы убедиться, что воркер корректно завершает работу.

  • Повторное соединение При перезагрузке страницы или повторном открытии вкладки создаётся новый порт. Подготовьте первичное сообщение синхронизации (чтобы отправить всё состояние сразу).

  • Обработка перегрузки (Backpressure) При интенсивной рассылке сообщений используйте throttling/debouncing (ограничение частоты) или отправку снимков состояния.

  • Безопасность Shared Worker по своей природе разделяется только внутри одного origin (источника). Если предполагается хранить секретные данные внутри работника, подумайте о механизмах верификации через токены или аналогичные средства со стороны вызывающих страниц.

Как правильно использовать Dedicated Worker, Shared Worker и Service Worker

Каждый тип Worker имеет следующие особенности:.

  • Dedicated Worker Dedicated Worker предназначен только для использования одной страницей. Он обеспечивает разделение вычислений и пользовательского интерфейса по принципу 1:1.

  • Shared Worker Shared Worker может использоваться несколькими страницами с одним и тем же источником (origin). Он идеален для взаимодействия между вкладками и обеспечения общей связи.

  • Service Worker Service Worker может использоваться для проксирования сетевых запросов, кэширования, офлайн-операций, push-уведомлений и фоновой синхронизации. Его сильная сторона — возможность перехватывать сетевые запросы (fetch).

Общее правило: используйте Shared Worker для «обмена информацией и произвольной обработки между вкладками», Service Worker — для «управления сетью», а Dedicated Worker — если нужно просто разгрузить UI от тяжелых вычислений.

Типичные ошибки

При использовании Shared Worker необходимо учитывать следующие моменты.

  • Забыв вызвать start() или лишние вызовы этого метода При использовании port.addEventListener('message', ...), необходимо вызвать port.start(). Это не требуется, если вы используете port.onmessage = ....

  • Неограниченная широковещательная рассылка Нагрузка увеличивается по мере роста числа вкладок. Можно снизить нагрузку, реализовав дифференцированную доставку или подписку на темы (фильтрацию по теме).

  • Затраты на копирование объектов postMessage дублирует данные. Для больших объемов данных рассмотрите передачу их как Transferable (например, ArrayBuffer) или использование разделяемой памяти (SharedArrayBuffer) совместно с Atomics.

  • Время жизни и повторная инициализация Shared Worker может завершиться после отключения последнего клиента. Корректно продумайте инициализацию для первичных подключений и перезапусков, чтобы избежать побочных эффектов и ошибок.

Резюме

  • Shared Worker используется для реализации долго работающей кастомной логики, доступной на нескольких страницах.
  • Определяйте чётко протоколы и типы сообщений, делайте механику запросов/ответов надёжной, используя корреляционные идентификаторы (correlation IDs).
  • Он идеально подходит для процессов, которые выгоднее обрабатывать единожды для всех вкладок, например, для мультиплексирования WebSocket-подключений или сериализации доступа к IndexedDB.
  • Тонкая настройка таких деталей, как сборщики, типы данных и механизмы переподключения, может значительно повысить удобство сопровождения и пользовательский опыт.

Вы можете следовать этой статье, используя Visual Studio Code на нашем YouTube-канале. Пожалуйста, также посмотрите наш YouTube-канал.

YouTube Video