Реализация Background Sync для PWA
Background Sync позволяет Service Worker выполнить отложенное действие (отправить форму, синхронизировать данные) при восстановлении соединения — даже если пользователь уже закрыл вкладку.
Как работает Background Sync
- Пользователь выполняет действие (кладёт товар в корзину) — нет интернета
- Приложение сохраняет задачу в IndexedDB и регистрирует sync-тег
- Браузер ждёт сетевого соединения
- Service Worker получает событие
syncи выполняет отложенную задачу - При неудаче — браузер повторит попытку с экспоненциальной задержкой
Регистрация sync из страницы
// background-sync.ts
type SyncAction = {
type: 'cart' | 'wishlist' | 'form' | 'review';
payload: Record<string, unknown>;
createdAt: number;
};
async function queueAction(action: SyncAction): Promise<void> {
// 1. Сохранить в IndexedDB
const db = await openDatabase();
await db.put('syncQueue', { ...action, id: Date.now() });
// 2. Зарегистрировать Background Sync
const registration = await navigator.serviceWorker.ready;
if ('sync' in registration) {
await (registration as any).sync.register(`sync-${action.type}`);
} else {
// Fallback для браузеров без Background Sync
if (navigator.onLine) {
await processAction(action);
}
}
}
// Открытие IndexedDB
async function openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open('PWASync', 1);
request.onupgradeneeded = e => {
(e.target as IDBOpenDBRequest).result
.createObjectStore('syncQueue', { keyPath: 'id' });
};
request.onsuccess = e => resolve((e.target as IDBOpenDBRequest).result);
request.onerror = reject;
});
}
Service Worker: обработка sync-событий
// sw.js
self.addEventListener('sync', event => {
console.log('Background sync triggered:', event.tag);
switch (event.tag) {
case 'sync-cart':
event.waitUntil(syncCart());
break;
case 'sync-wishlist':
event.waitUntil(syncWishlist());
break;
case 'sync-form':
event.waitUntil(syncPendingForms());
break;
case 'sync-review':
event.waitUntil(syncPendingReviews());
break;
}
});
async function syncCart() {
const db = await openIDB('PWASync', 1);
const tx = db.transaction('syncQueue', 'readwrite');
const store = tx.objectStore('syncQueue');
const actions = await getAllFromStore(store, 'cart');
for (const action of actions) {
const response = await fetch('/api/cart/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Sync': 'background',
},
body: JSON.stringify(action.payload),
});
if (response.ok) {
await store.delete(action.id);
// Уведомить открытые вкладки о синхронизации
const clients = await self.clients.matchAll();
clients.forEach(client => {
client.postMessage({ type: 'CART_SYNCED', payload: action.payload });
});
} else if (response.status >= 400 && response.status < 500) {
// Клиентская ошибка — удалить, не повторять
await store.delete(action.id);
}
// 5xx — оставить для повтора (браузер повторит sync автоматически)
}
}
Periodic Background Sync (Chrome)
Позволяет выполнять задачи по расписанию — обновлять курс валют, новости, прогноз погоды:
// Регистрация
async function registerPeriodicSync() {
const registration = await navigator.serviceWorker.ready;
if ('periodicSync' in registration) {
const status = await navigator.permissions.query({ name: 'periodic-background-sync' as any });
if (status.state === 'granted') {
await (registration as any).periodicSync.register('update-prices', {
minInterval: 60 * 60 * 1000, // не чаще раза в час
});
}
}
}
// sw.js: periodic sync
self.addEventListener('periodicsync', event => {
if (event.tag === 'update-prices') {
event.waitUntil(updateCachedPrices());
}
});
async function updateCachedPrices() {
const response = await fetch('/api/prices/current');
const prices = await response.json();
const cache = await caches.open('dynamic-v1');
// Обновить закешированные данные
await cache.put('/api/prices/current', new Response(JSON.stringify(prices), {
headers: { 'Content-Type': 'application/json' }
}));
// Уведомить пользователя если цена на избранный товар изменилась
await checkWishlistPriceChanges(prices);
}
Отображение статуса синхронизации
// useSyncStatus.ts
export function useSyncStatus() {
const [pendingCount, setPendingCount] = useState(0);
const [isSyncing, setIsSyncing] = useState(false);
useEffect(() => {
// Слушать сообщения от Service Worker
const handler = (event: MessageEvent) => {
if (event.data.type === 'CART_SYNCED') {
setPendingCount(c => Math.max(0, c - 1));
setIsSyncing(false);
}
if (event.data.type === 'SYNC_STARTED') {
setIsSyncing(true);
}
};
navigator.serviceWorker.addEventListener('message', handler);
return () => navigator.serviceWorker.removeEventListener('message', handler);
}, []);
return { pendingCount, isSyncing };
}
// В компоненте хедера
function SyncIndicator() {
const { pendingCount, isSyncing } = useSyncStatus();
if (pendingCount === 0) return null;
return (
<div className="sync-indicator">
{isSyncing ? 'Синхронизация...' : `${pendingCount} действий ожидают синхронизации`}
</div>
);
}
Срок реализации: 1–2 дня для базового Background Sync с корзиной и формами.







