Общий работник (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 WorkerDedicated Workerпредназначен только для использования одной страницей. Он обеспечивает разделение вычислений и пользовательского интерфейса по принципу 1:1. -
Shared WorkerShared Workerможет использоваться несколькими страницами с одним и тем же источником (origin). Он идеален для взаимодействия между вкладками и обеспечения общей связи. -
Service WorkerService 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-канал.