Реалізація оффлайн-режиму для PWA

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація оффлайн-режиму для PWA
Середня
~2-3 робочих дні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація офлайн-режиму для PWA

Офлайн-режим дозволяє приложенню частково функціонувати без інтернету: показувати закешовані сторінки, приймати замовлення для подальшої синхронізації, відображати останні дані замість помилки. Реалізується через Service Worker + IndexedDB.

Що кешувати для офлайну

App Shell — мінімальний HTML/CSS/JS для роботи інтерфейсу. Кешується при установці Service Worker.

Дані — останні завантажені сторінки, вибране користувача, кошик. Кешуються при виконанні.

// sw.js: стратегія для різних типів контенту

const SHELL_CACHE    = 'shell-v1';
const CONTENT_CACHE  = 'content-v1';
const IMAGES_CACHE   = 'images-v1';

const APP_SHELL = ['/', '/cart', '/wishlist', '/offline.html'];

// Кешувати всі відвідані HTML-сторінки
self.addEventListener('fetch', event => {
    if (event.request.headers.get('Accept')?.includes('text/html')) {
        event.respondWith(networkFirstWithOfflineFallback(event.request));
    }
});

async function networkFirstWithOfflineFallback(request) {
    const cache = await caches.open(CONTENT_CACHE);

    try {
        const response = await Promise.race([
            fetch(request),
            new Promise((_, reject) => setTimeout(reject, 3000, new Error('timeout')))
        ]);

        cache.put(request, response.clone());
        return response;
    } catch {
        const cached = await cache.match(request);
        if (cached) return cached;

        // Повернути офлайн-сторінку з поясненням
        return caches.match('/offline.html');
    }
}

Індикатор стану мережі

// useNetworkStatus.ts
export function useNetworkStatus() {
    const [isOnline, setIsOnline] = useState(navigator.onLine);
    const [wasOffline, setWasOffline] = useState(false);

    useEffect(() => {
        const handleOnline = () => {
            setIsOnline(true);
            if (wasOffline) {
                // Синхронізувати відкладені дії
                syncPendingActions();
                setWasOffline(false);
            }
        };

        const handleOffline = () => {
            setIsOnline(false);
            setWasOffline(true);
        };

        window.addEventListener('online', handleOnline);
        window.addEventListener('offline', handleOffline);
        return () => {
            window.removeEventListener('online', handleOnline);
            window.removeEventListener('offline', handleOffline);
        };
    }, [wasOffline]);

    return { isOnline, wasOffline };
}

// У компоненті
function OfflineBanner() {
    const { isOnline } = useNetworkStatus();

    if (isOnline) return null;

    return (
        <div className="offline-banner">
            Немає з'єднання. Показані збережені дані.
        </div>
    );
}

IndexedDB для офлайн-даних

// db.ts — Dexie.js (wrapper для IndexedDB)
import Dexie, { type Table } from 'dexie';

interface CachedProduct {
    id: number;
    slug: string;
    name: string;
    price: number;
    image: string;
    cachedAt: Date;
}

interface PendingAction {
    id?: number;
    type: 'add_to_cart' | 'add_to_wishlist' | 'submit_review';
    payload: Record<string, unknown>;
    createdAt: Date;
}

class AppDatabase extends Dexie {
    products!: Table<CachedProduct>;
    pendingActions!: Table<PendingAction>;

    constructor() {
        super('AppDatabase');
        this.version(1).stores({
            products:       'id, slug, cachedAt',
            pendingActions: '++id, type, createdAt',
        });
    }
}

export const db = new AppDatabase();

Відкладені дії (Optimistic UI)

// Додавання у кошик — працює офлайн
async function addToCart(productId: number, quantity: number) {
    const { isOnline } = useNetworkStatus();

    if (isOnline) {
        // Онлайн — звичайний запит
        await api.post('/cart/items', { productId, quantity });
    } else {
        // Офлайн — зберегти для синхронізації
        await db.pendingActions.add({
            type: 'add_to_cart',
            payload: { productId, quantity },
            createdAt: new Date(),
        });

        // Оновити локальний стан (optimistic)
        updateCartLocally(productId, quantity);

        // Сповістити користувача
        showToast('Товар додано. Синхронізується при підключенні');
    }
}

// Синхронізація при відновленні з'єднання
async function syncPendingActions() {
    const pending = await db.pendingActions.toArray();

    for (const action of pending) {
        try {
            await processAction(action);
            await db.pendingActions.delete(action.id!);
        } catch (err) {
            console.error('Sync failed for action:', action, err);
        }
    }
}

Background Sync через Service Worker

// sw.js: автоматична синхронізація при відновленні з'єднання
self.addEventListener('sync', event => {
    if (event.tag === 'sync-cart') {
        event.waitUntil(syncCartItems());
    }
    if (event.tag === 'sync-reviews') {
        event.waitUntil(syncPendingReviews());
    }
});

async function syncCartItems() {
    const db = await openDB('AppDatabase', 1);
    const pending = await db.getAll('pendingActions');

    for (const action of pending.filter(a => a.type === 'add_to_cart')) {
        const response = await fetch('/api/cart/items', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(action.payload),
        });

        if (response.ok) {
            await db.delete('pendingActions', action.id);
        }
    }
}
// Реєстрація sync зі сторінки
async function registerBackgroundSync() {
    const registration = await navigator.serviceWorker.ready;
    if ('sync' in registration) {
        await (registration as SyncRegistration).sync.register('sync-cart');
    }
}

Офлайн для каталогу з пошуком

// Кешувати останні результати пошуку
const SEARCH_CACHE_SIZE = 20;

async function search(query: string): Promise<Product[]> {
    const { isOnline } = getNetworkStatus();

    if (isOnline) {
        const results = await api.get('/search', { params: { q: query } });
        // Кешувати результат
        await db.searchCache.put({ query, results, cachedAt: new Date() });
        return results;
    } else {
        // Офлайн — пошук за IndexedDB
        const cached = await db.searchCache.get(query);
        if (cached) return cached.results;

        // Локальний пошук по закешованим продуктам
        return db.products
            .filter(p => p.name.toLowerCase().includes(query.toLowerCase()))
            .toArray();
    }
}

Час реалізації: 2–3 дні для повного офлайн-режиму з IndexedDB та Background Sync.