Реализация 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-соединение на весь браузер, кеш данных, shared auth-state, distributed lock между вкладками.

Поддержка: Chrome, Firefox, Edge. Safari поддерживает SharedWorker с версии 16 (2022), но с ограничениями. Для критически важной межтабовой коммуникации стоит также смотреть на BroadcastChannel (проще, но без shared state) и localStorage events.

Архитектура SharedWorker

Воркер хранит Map соединений и рассылает сообщения всем подключённым вкладкам:

// 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)
    // Обновить UI без полной перезагрузки
    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 как альтернатива

Для простой межтабовой коммуникации без shared state 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+), но не имеет shared state и не позволяет создать единственное WebSocket-соединение.

Отладка

SharedWorker виден в Chrome DevTools:

  • about:inspect → Shared workers
  • Или через chrome://inspect/#workers

Воркер не перезапускается при перезагрузке страницы — нужно явно закрыть вкладку или через DevTools.

Что входит в работу

Реализация SharedWorker с поддержкой broadcast и shared state, клиентский класс с типизацией, обработка подключения/отключения вкладок, опционально — WebSocket-мост или синхронизация аутентификации, fallback на BroadcastChannel для несовместимых браузеров.

Срок: 2–3 дня в зависимости от сценариев (auth sync, WebSocket bridge, shared cache).