Впровадження SharedWorker для міжтабового взаємодії на веб-сайті
SharedWorker — Web Worker, який розділяється між усіма вкладками, фреймами і вікнами одного origin. Один екземпляр воркера обслуговує N вкладок через MessagePort-и. Коли всі вкладки закриті — воркер знищується.
Застосування: синхронізація стану між вкладками без сервера, єдине WebSocket-з'єднання на весь браузер, спільний кеш, спільний auth-стан, розподілене блокування між вкладками.
Підтримка: Chrome, Firefox, Edge. Safari підтримує SharedWorker з версії 16 (2022), але з обмеженнями. Для критично важливої міжтабової комунікації варто також розглянути BroadcastChannel (простіше, але без спільного стану) та localStorage события.
Архітектура SharedWorker
Воркер зберігає Set з'єднань і розсилає повідомлення всім підключеним вкладкам:
// shared-worker.ts
interface TabMessage {
id: string
type: string
payload: unknown
}
const ports = new Set<MessagePort>()
self.addEventListener('connect', (event: MessageEvent) => {
const port = event.ports[0]
ports.add(port)
port.addEventListener('message', (e: MessageEvent) => {
const message = e.data as TabMessage
handleMessage(message, port)
})
port.addEventListener('messageerror', (e) => {
console.error('SharedWorker message error:', e)
})
port.start()
// Повідомити воркер, що нова вкладка підключилась
port.postMessage({ type: 'CONNECTED', payload: { tabCount: ports.size } })
port.addEventListener('close', () => {
ports.delete(port)
broadcast({ type: 'TAB_COUNT', payload: { count: ports.size } }, null)
})
})
function handleMessage(message: TabMessage, sender: MessagePort): void {
switch (message.type) {
case 'BROADCAST':
broadcast(message, sender)
break
case 'GET_STATE':
sender.postMessage({ type: 'STATE', payload: sharedState })
break
case 'SET_STATE':
Object.assign(sharedState, message.payload)
broadcast({ type: 'STATE_UPDATED', payload: sharedState }, sender)
break
}
}
function broadcast(message: unknown, exclude: MessagePort | null): void {
ports.forEach((port) => {
if (port !== exclude) {
port.postMessage(message)
}
})
}
// Спільний стан для всіх вкладок
const sharedState: Record<string, unknown> = {}
Клієнтський клас
// SharedWorkerClient.ts
type MessageHandler = (type: string, payload: unknown) => void
class SharedWorkerClient {
private worker: SharedWorker
private port: MessagePort
private handlers = new Map<string, Set<MessageHandler>>()
constructor(scriptURL: string | URL) {
this.worker = new SharedWorker(scriptURL, { type: 'module', name: 'app-shared' })
this.port = this.worker.port
this.port.onmessage = (event: MessageEvent) => {
const { type, payload } = event.data
this.emit(type, payload)
}
this.port.onmessageerror = (e) => {
console.error('Port error:', e)
}
this.port.start()
}
on(type: string, handler: MessageHandler): () => void {
if (!this.handlers.has(type)) {
this.handlers.set(type, new Set())
}
this.handlers.get(type)!.add(handler)
return () => this.handlers.get(type)?.delete(handler)
}
private emit(type: string, payload: unknown): void {
this.handlers.get(type)?.forEach((h) => h(type, payload))
this.handlers.get('*')?.forEach((h) => h(type, payload))
}
send(type: string, payload?: unknown): void {
this.port.postMessage({ type, payload })
}
broadcast(type: string, payload?: unknown): void {
this.port.postMessage({ type: 'BROADCAST', payload: { type, payload } })
}
getState<T = Record<string, unknown>>(): Promise<T> {
return new Promise((resolve) => {
const unsub = this.on('STATE', (_, payload) => {
unsub()
resolve(payload as T)
})
this.send('GET_STATE')
})
}
setState(patch: Record<string, unknown>): void {
this.send('SET_STATE', patch)
}
close(): void {
this.port.close()
}
}
WebSocket через SharedWorker
Замість того щоб кожна вкладка створювала окреме WebSocket-з'єднання, всі вкладки використовують одне:
// shared-worker.ts — WebSocket частина
let socket: WebSocket | null = null
let reconnectTimer: ReturnType<typeof setTimeout>
function connectSocket(url: string): void {
if (socket?.readyState === WebSocket.OPEN) return
socket = new WebSocket(url)
socket.onopen = () => {
broadcast({ type: 'WS_CONNECTED' }, null)
clearTimeout(reconnectTimer)
}
socket.onmessage = (event) => {
const data = JSON.parse(event.data)
broadcast({ type: 'WS_MESSAGE', payload: data }, null)
}
socket.onerror = () => {
broadcast({ type: 'WS_ERROR' }, null)
}
socket.onclose = () => {
broadcast({ type: 'WS_DISCONNECTED' }, null)
// Автоматичне переподключення
reconnectTimer = setTimeout(() => connectSocket(url), 3000)
}
}
// У handleMessage:
case 'WS_CONNECT':
connectSocket(message.payload as string)
break
case 'WS_SEND':
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message.payload))
}
break
Синхронізація аутентифікації
Реальний сценарій: користувач розлогінився в одній вкладці — всі інші повинні перенаправити на /login:
// auth-sync.ts
const sharedWorker = new SharedWorkerClient(
new URL('./shared-worker.ts', import.meta.url)
)
export function setupAuthSync(): () => void {
const unsub = sharedWorker.on('AUTH_LOGOUT', () => {
// Видалити токени та перенаправити
localStorage.removeItem('token')
window.location.href = '/login'
})
const unsubLogin = sharedWorker.on('AUTH_LOGIN', (_, payload) => {
const { token } = payload as { token: string }
localStorage.setItem('token', token)
// Оновити інтерфейс без повної перезагрузки
window.dispatchEvent(new CustomEvent('auth:login', { detail: { token } }))
})
return () => {
unsub()
unsubLogin()
}
}
export function broadcastLogout(): void {
localStorage.removeItem('token')
sharedWorker.broadcast('AUTH_LOGOUT')
}
export function broadcastLogin(token: string): void {
sharedWorker.broadcast('AUTH_LOGIN', { token })
}
BroadcastChannel як альтернатива
Для простої міжтабової комунікації без спільного стану SharedWorker може бути надлишковим:
const channel = new BroadcastChannel('app-events')
// Відправити всім вкладкам (крім поточної)
channel.postMessage({ type: 'CART_UPDATED', payload: cartItems })
// Прийняти
channel.onmessage = (event) => {
const { type, payload } = event.data
if (type === 'CART_UPDATED') updateCartUI(payload)
}
channel.close()
BroadcastChannel простіший, працює везде (включаючи Safari 15.4+), але не має спільного стану і не дозволяє створити єдине WebSocket-з'єднання.
Відладка
SharedWorker видно у Chrome DevTools:
-
about:inspect→ Shared workers - Або через
chrome://inspect/#workers
Воркер не перезапускається при перезагрузці сторінки — потрібно явно закрити вкладку або через DevTools.
Що включено
Впровадження SharedWorker з підтримкою broadcast та спільного стану, типізований клієнтський клас, обробка підключення/відключення вкладок, опціонально — WebSocket-міст або синхронізація аутентифікації, fallback на BroadcastChannel для несумісних браузерів.
Терміни: 2–3 дні залежно від сценаріїв (auth sync, WebSocket bridge, shared cache).







