Реалізація SharedWorker для міжтабової взаємодії на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація SharedWorker для міжтабової взаємодії на сайті
Складна
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Впровадження 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).