При работе с современными веб-приложениями часто возникает задача синхронизации состояния между несколькими вкладками одного и того же сайта. Например, если пользователь изменил настройки или обновил данные в одной вкладке, нужно, чтобы остальные вкладки сразу отразили эти изменения. В сочетании с библиотекой Idb-keyval это становится особенно удобным и эффективным, так как Idb-keyval обеспечивает простой интерфейс для работы с IndexedDB, а 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') должно
совпадать во всех вкладках, которые должны обмениваться данными.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 позволяет реализовать мгновенную синхронизацию данных.
const channel = new BroadcastChannel('app-state');
async function saveSettings(settings) {
await se t('userSettings', settings);
channel.postMessage({ type: 'UPDATE_STATE', payload: settings });
}
channel.onmess age = async (event) => {
if (event.data.type === 'UPDATE_STATE') {
// Сохраняем данные локально в IndexedDB
await set('userSettings', event.data.payload);
// Обновляем интерфейс
applySettings(event.data.payload);
}
};
set и
update должны быть спроектированы так, чтобы повторное
применение данных не ломало состояние.Если несколько вкладок одновременно изменяют одни и те же данные, возможны конфликты. Для их предотвращения:
const newState = { ...settings, upd atedAt: Date.now() };
await se t('userSettings', newState);
channel.postMessage({ type: 'UPDATE_STATE', payload: newState });
При получении состояния проверять upd atedAt и применять
только более новое.
Выделять локального лидера (опционально) для критических операций. Например, одна вкладка может быть «главной», выполняющей запись в базу, а остальные только синхронизируют.
Применять 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 для разрешения
конфликтов.window.addEventListener('beforeunload', () => channel.close());
type: 'SETTINGS_UPDATE',
type: 'NOTIFICATIONS_UPDATE').BroadcastChannel и Idb-keyval создают мощное сочетание для реактивного, синхронного и безопасного обмена состоянием между вкладками, минимизируя риски потери данных и упрощая работу с локальным хранилищем.