Реалізація 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 з кошиком та формами.







