Общий работник (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 & 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-канал.