Синхронизация состояния между вкладками через BroadcastChannel

При работе с современными веб-приложениями часто возникает задача синхронизации состояния между несколькими вкладками одного и того же сайта. Например, если пользователь изменил настройки или обновил данные в одной вкладке, нужно, чтобы остальные вкладки сразу отразили эти изменения. В сочетании с библиотекой Idb-keyval это становится особенно удобным и эффективным, так как Idb-keyval обеспечивает простой интерфейс для работы с IndexedDB, а BroadcastChannel позволяет передавать сообщения между вкладками в реальном времени.


Основы BroadcastChannel

BroadcastChannel — это встроенный API браузера для обмена сообщениями между вкладками или окнами одного происхождения (origin). Основные моменты использования:

const channel = new BroadcastChannel('app-state');

channel.onmess age = (event) => {
    console.log('Получено сообщение:', event.data);
};

// Отправка сообщения другим вкладкам
channel.postMessage({ type: 'UPDATE_STATE', payload: {...} });
  • Имя канала ('app-state') должно совпадать во всех вкладках, которые должны обмениваться данными.
  • Сообщения доставляются всем открытым вкладкам, кроме той, которая отправила сообщение, если явно не обработать это в коде.
  • Поддержка современных браузеров достаточно широкая, включая Chrome, Firefox, Edge и Safari.

Хранение состояния с Idb-keyval

Idb-keyval — это обертка над IndexedDB, предоставляющая простой API для асинхронного сохранения ключ-значение. Основные методы:

import { set, get, del, upd ate } from 'idb-keyval';

// Сохранение значения
await se t('userSettings', { theme: 'dark', fontSize: 16 });

// Получение значения
const settings = await get('userSettings');

// Обновление значения через коллбек
await upd ate('userSettings', old => ({ ...old, fontSize: 18 }));

// Удаление значения
await del('userSettings');

Idb-keyval позволяет работать с локальным хранилищем асинхронно и безопасно для разных вкладок.


Синхронизация состояния между вкладками

Комбинация BroadcastChannel и Idb-keyval позволяет реализовать мгновенную синхронизацию данных.

Пример схемы синхронизации

  1. Создание канала:
const channel = new BroadcastChannel('app-state');
  1. Отправка изменений после сохранения в IndexedDB:
async function saveSettings(settings) {
    await se t('userSettings', settings);
    channel.postMessage({ type: 'UPDATE_STATE', payload: settings });
}
  1. Обработка сообщений из других вкладок:
channel.onmess age = async (event) => {
    if (event.data.type === 'UPDATE_STATE') {
        // Сохраняем данные локально в IndexedDB
        await set('userSettings', event.data.payload);

        // Обновляем интерфейс
        applySettings(event.data.payload);
    }
};
  • Ключевой момент: каждая вкладка может сохранять данные и одновременно реагировать на изменения, приходящие через канал, избегая конфликтов.
  • Idempotентность: операции set и update должны быть спроектированы так, чтобы повторное применение данных не ломало состояние.

Управление потенциальными конфликтами

Если несколько вкладок одновременно изменяют одни и те же данные, возможны конфликты. Для их предотвращения:

  1. Использовать временные метки:
const newState = { ...settings, upd atedAt: Date.now() };
await se t('userSettings', newState);
channel.postMessage({ type: 'UPDATE_STATE', payload: newState });

При получении состояния проверять upd atedAt и применять только более новое.

  1. Выделять локального лидера (опционально) для критических операций. Например, одна вкладка может быть «главной», выполняющей запись в базу, а остальные только синхронизируют.

  2. Применять update вместо se t, если необходимо корректно объединять изменения:

await upd ate('userSettings', old => ({ ...old, ...incomingChanges }));

Пример комплексного решения

import { se t, get, upd ate } from 'idb-keyval';

const channel = new BroadcastChannel('app-state');

channel.onmess age = async (event) => {
    if (event.data.type === 'UPDATE_STATE') {
        const localState = await get('userSettings') || {};
        const incoming = event.data.payload;
        if (!localState.updatedAt || incoming.updatedAt > localState.updatedAt) {
            await se t('userSettings', incoming);
            applySettings(incoming);
        }
    }
};

async function saveSettings(settings) {
    const newState = { ...settings, upd atedAt: Date.now() };
    await se t('userSettings', newState);
    channel.postMessage({ type: 'UPDATE_STATE', payload: newState });
}

function applySettings(settings) {
    document.body.dataset.theme = settings.theme;
    document.body.style.fontSize = settings.fontSize + 'px';
}
  • Используется updatedAt для разрешения конфликтов.
  • Каждая вкладка может как отправлять изменения, так и принимать их, поддерживая синхронное состояние.
  • Интерфейс автоматически обновляется при получении новых данных.

Рекомендации по производительности

  • Не отправлять через канал объёмные данные; лучше отправлять только ключи и минимальный набор изменений, а остальные вкладки подтягивают полный объект из IndexedDB.
  • Закрывать канал при выгрузке страницы:
window.addEventListener('beforeunload', () => channel.close());
  • Использовать debounce для частых изменений, чтобы избежать «шторма» сообщений между вкладками.

Возможные расширения

  • Синхронизация нескольких сущностей через один канал с разными типами сообщений (type: 'SETTINGS_UPDATE', type: 'NOTIFICATIONS_UPDATE').
  • Хранение истории изменений для отката при ошибках.
  • Интеграция с Service Worker для синхронизации с сервером, сохраняя согласованность между вкладками и устройствами пользователя.

BroadcastChannel и Idb-keyval создают мощное сочетание для реактивного, синхронного и безопасного обмена состоянием между вкладками, минимизируя риски потери данных и упрощая работу с локальным хранилищем.