Способы передачи сообщений между вкладками и окнами браузера

Существует целый ряд задач, для решения которых необходимо передавать состояние между разными контекстами браузера. Контекстами браузера могут быть вкладки, окна и встроенные на страницу фреймы. В этой статье я хотел бы рассмотреть способы решать задачи, которые требуют передачи сообщений между вкладками и окнами в пределах одного и того же ресурса (same-origin).

Примеры подобных задач:

  • обновление аутентификационного токена (auth token) во всех контекстах при обновлении токена в одном из них;
  • разлогин пользователя во всех контекстах, когда завершается сессия в одном из них;
  • синхронизация клиентского состояния между контекстами или изменение клиентского состояния в других контекстах как реакция на событие в одном из них;
  • оптимистичное обновление UI во всех открытых контекстах без необходимости синхронизации с сервером (через механизм поллинга или вебсокеты);
  • остановка действия во всех контекстах, кроме активного (например, остановка проигрывания аудио в приложении веб-плеера в одной вкладке, если проигрывание было запущено в другой).

Давайте рассмотрим самые популярные способы решать подобные задачи:

Web Storage API

Web Storage API предоставляет интерфейсы для работы с хранилищами localStorage и sessionStorage. Эти интерфейсы позволяют добавить строковую пару ключ-значение в объект Storage, после чего будет сгенерировано событие storage во всех вкладках, кроме той, через которую вносилось изменение. Внутри обработчика события storage можно получить доступ не только к новому, но и к предыдущему значению элемента, который был изменён.

👀 Пример использования:

Инициация события storage:

localStorage.setItem("key", "value");

Обработка события storage:

window.addEventListener("storage", (e) => {
  console.log(e.newValue); // "value"
});

Плюсы:

  • простой API
  • самая широкая поддержка в браузерах

Минусы:

  • не подходит для синхронизации чувствительной информации типа auth tokens
  • имеет ограничения по размеру хранимой информации
  • неудобно для работы со сложной логикой
  • работает синхронно, поэтому может заблокировать основной UI-поток при передаче больших чанков информации

Shared Worker

Shared Worker — это скрипт, который могут переиспользовать разные контексты браузера в пределах одного ресурса. Скрипт работает в отдельном потоке и не влияет на основной поток выполнения кода приложения. Shared Worker может получать сообщения от вкладок, которые к нему подключены, хранить и обрабатывать эти сообщения, а также уведомлять остальные вкладки о результатах обработки таких сообщений.

👀 Пример использования:

Скрипт, который умеет принимать сообщения и отправлять их обратно в приложение:

// shared-worker.js

self.addEventListener("connect", (e) => {
  e.source.addEventListener("message", (event) => {
    port.postMessage(event.data);
  });

  e.source.start();
});

Подключение приложения к Shared Worker и получение/отправка сообщений:

const worker = new SharedWorker("shared-worker.js");

worker.port.addEventListener("message", (e) => {
  console.log(e.data);
});

worker.port.start();

worker.port.postMessage("message");

Плюсы:

  • работает в отдельном потоке, подходит для независимого выполнения тяжёлых вычислений
  • может содержать сложную логику и работать с разными структурами данных
  • предоставляет контроль над источниками и потребителями сообщений

Минусы:

  • не самая широкая поддержка браузерами
  • высокий порог вхождения для решения простых кейсов

Broadcast Channel API

Broadcast Channel API — это нативный браузерный интерфейс, который придуман специально для коммуникации между вкладками и окнами одного и того же приложения. Синтаксис Broadcast Channel API очень простой и позволяет гибко настраивать синхронизацию между контекстами, а также безопасен для передачи чувствительной информации.

👀 Пример использования:

Инициация события отправки сообщения:

const bc = new BroadcastChannel("channel");

bc.postMessage("message");

bc.close();

Получение и обработка сообщения в других вкладках:

const bc = new BroadcastChannel("channel");

bc.addEventListener("message", (e) => {
  console.log(e.data);
});

bc.close();

Плюсы:

  • простой API
  • хорошо поддерживается браузерами на сегодняшний день
  • подходит для передачи чувствительных данных, требующих защиты от cross-site-scripting (XSS) атак

Минусы:

  • меньше контроля над источниками и получателями сообщений, сообщения транслируются сразу всем вкладкам

В заключение

Учитывая простоту, удобство использования и широкую поддержку современными браузерами, Broadcast Channel API будет лучшим решением в подавляющем количестве кейсов, в которых необходимо настроить общение между вкладками в пределах одного ресурса.

Shared Worker стоит использовать только для очень сложных задач, в которых каждая вкладка обрабатывается как отдельный источник/потребитель данных со своей уникальной логикой. В этом случае воркер выступает хранилищем общего состояния всего приложения, когда как вкладки становятся независимыми потребителями этого состояния. Возможно, что для решения таких сложных задач пригодится ещё более мощный инструмент — Service Worker, который не имеет своего состояния, но позволяет тонко настраивать логику работы с внешними ресурсами и синхронизировать результаты этой работы между вкладками через встроенный механизм postMessage.

localStorage (sessionStorage) и событие storage можно использовать для совсем простых кейсов и передачи очень небольших чанков информации, так как передача больших чанков может приводить к блокировкам потока, отвечающего за работу с UI. Web Storage API поддерживается даже IE, так что его можно использовать в целях поддержания кроссбраузерности вашего решения.

В этой статье я не рассматривал глобальный метод window.postMessage, так как он работает только с теми вкладками и окнами, которые были созданы методами window.open() или document.open(), требуя указания ссылки на созданные таким образом контексты. Лучше всего использовать этот способ для общения между фреймами, которые были подключены на одной странице и живут в разных ресурсах (cross-origin).


Для более глубокого погружения в материал советую посмотреть уроки от Tom Gobich с разбором примеров использования каждого способа:


Приглашаю вас подписаться на мой телеграм-канал: https://t.me/alexgriss , где я пишу о фронтенде, архитектуре, UX/UI, продакт-мышлении, стартапах и лидерстве, а так же о том, как оставаться собой в профессии и создавать то, что имеет значение.