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

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация оффлайн-режима для PWA
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1243
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1168
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    873
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1086
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    830
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    847

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

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

Что кешировать для офлайна

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

Данные — последние загруженные страницы, избранное пользователя, корзина. Кешируются в runtime.

// 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 update)
        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.