← Назад к вопросам

Как реализована поддержка Offline режима в текущем проекте?

2.3 Middle🔥 191 комментариев
#JavaScript Core

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Поддержка Offline режима в проекте

Оффлайн режим - важная фича для PWA приложений. Есть несколько стратегий и инструментов для её реализации.

1. Service Workers - основа offline функционала

Service Worker это скрипт, который работает в отдельном потоке и перехватывает сетевые запросы:

// public/sw.js
const CACHE_NAME = 'prepbro-v1';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/favicon.ico'
];

// Установка Service Worker
self.addEventListener('install', (event) => {
  console.log('Service Worker установлен');
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('Кешируем статические файлы');
      return cache.addAll(STATIC_ASSETS);
    })
  );
});

// Активация
self.addEventListener('activate', (event) => {
  console.log('Service Worker активирован');
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== CACHE_NAME) {
            console.log('Удаляю старый cache:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

// Перехват fetch запросов (Network First)
self.addEventListener('fetch', (event) => {
  // Пропускаем non-GET запросы
  if (event.request.method !== 'GET') {
    return;
  }

  event.respondWith(
    // Сначала пробуем сеть
    fetch(event.request)
      .then((response) => {
        // Если успешный ответ - кешируем и возвращаем
        if (!response.ok) throw new Error('Network error');

        const cache = caches.open(CACHE_NAME);
        cache.then((c) => c.put(event.request, response.clone()));
        return response;
      })
      .catch(() => {
        // Если сеть недоступна - берём из кеша
        return caches.match(event.request);
      })
  );
});

2. Регистрация Service Worker в приложении

// lib/serviceWorker.ts
export const registerServiceWorker = async () => {
  if (!('serviceWorker' in navigator)) {
    console.log('Service Worker не поддерживается');
    return;
  }

  try {
    const registration = await navigator.serviceWorker.register(
      '/sw.js',
      { scope: '/' }
    );
    console.log('Service Worker зарегистрирован:', registration);

    // Проверяем обновления каждый час
    setInterval(() => {
      registration.update();
    }, 60 * 60 * 1000);
  } catch (error) {
    console.error('Ошибка регистрации Service Worker:', error);
  }
};

// В главном компоненте приложения
import { useEffect } from 'react';
import { registerServiceWorker } from '@/lib/serviceWorker';

export function App() {
  useEffect(() => {
    registerServiceWorker();
  }, []);

  return (
    <div>
      {/* Содержимое приложения */}
    </div>
  );
}

3. Стратегии кеширования

// Network First - используй свежие данные, fallback на кеш
const networkFirst = async (request) => {
  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open('dynamic');
      cache.put(request, response.clone());
    }
    return response;
  } catch {
    return caches.match(request);
  }
};

// Cache First - используй кеш, fallback на сеть
const cacheFirst = async (request) => {
  const cached = await caches.match(request);
  if (cached) return cached;
  
  try {
    const response = await fetch(request);
    const cache = await caches.open('dynamic');
    cache.put(request, response.clone());
    return response;
  } catch {
    return new Response('Offline');
  }
};

// Stale While Revalidate - вернуть кеш сразу, обновить в фоне
const staleWhileRevalidate = async (request) => {
  const cached = await caches.match(request);
  
  const fetchPromise = fetch(request).then((response) => {
    const cache = caches.open('dynamic');
    cache.then((c) => c.put(request, response.clone()));
    return response;
  });

  return cached || fetchPromise;
};

4. Обнаружение состояния Online/Offline

// hooks/useOnlineStatus.ts
import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(
    typeof navigator !== 'undefined' ? navigator.onLine : true
  );

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

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

  return isOnline;
}

// Использование в компоненте
function NetworkStatus() {
  const isOnline = useOnlineStatus();

  return (
    <div>
      {isOnline ? (
        <span className='text-green-500'>Online</span>
      ) : (
        <span className='text-red-500'>Offline - данные из кеша</span>
      )}
    </div>
  );
}

5. Кеширование API данных

// lib/cache.ts
const API_CACHE = 'api-cache-v1';

export const fetchWithCache = async (
  url: string,
  options: RequestInit = {}
) => {
  const cache = await caches.open(API_CACHE);
  const cached = await cache.match(url);

  try {
    const response = await fetch(url, options);
    
    if (response.ok) {
      // Сохраняем успешный ответ
      cache.put(url, response.clone());
    }
    
    return response;
  } catch (error) {
    // Если нет интернета - возвращаем кешированный ответ
    if (cached) {
      return cached;
    }
    throw error;
  }
};

// Использование
const getQuestions = async (categoryId: string) => {
  const url = `/api/v1/questions?category=${categoryId}`;
  const response = await fetchWithCache(url);
  return response.json();
};

6. IndexedDB для сохранения состояния

// lib/db.ts
export class AppDB {
  private dbPromise: IDBDatabase;

  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('PrepBro', 1);
      
      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.dbPromise = request.result;
        resolve(this.dbPromise);
      };
      
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        
        // Создаём object stores
        if (!db.objectStoreNames.contains('questions')) {
          db.createObjectStore('questions', { keyPath: 'id' });
        }
        
        if (!db.objectStoreNames.contains('progress')) {
          db.createObjectStore('progress', { keyPath: 'userId' });
        }
      };
    });
  }

  async saveQuestions(questions: any[]) {
    const db = this.dbPromise;
    const tx = db.transaction('questions', 'readwrite');
    const store = tx.objectStore('questions');
    
    questions.forEach((q) => store.put(q));
    return tx.complete;
  }

  async getQuestions(categoryId: string) {
    const db = this.dbPromise;
    const tx = db.transaction('questions', 'readonly');
    const store = tx.objectStore('questions');
    
    return new Promise((resolve) => {
      const request = store.getAll();
      request.onsuccess = () => {
        const filtered = request.result.filter(
          (q) => q.categoryId === categoryId
        );
        resolve(filtered);
      };
    });
  }
}

const db = new AppDB();
await db.init();
await db.saveQuestions(questionsData);
const cached = await db.getQuestions(categoryId);

7. Синхронизация данных при возвращении Online

// hooks/useSyncQueue.ts
export function useSyncQueue() {
  const isOnline = useOnlineStatus();
  const [syncQueue, setSyncQueue] = useState([]);

  // Добавить операцию в очередь
  const queueOperation = (operation: Operation) => {
    const updated = [...syncQueue, operation];
    setSyncQueue(updated);
    // Сохранить в localStorage для персистентности
    localStorage.setItem('syncQueue', JSON.stringify(updated));
  };

  // При возвращении Online - синхронизировать
  useEffect(() => {
    if (!isOnline || syncQueue.length === 0) return;

    const syncAll = async () => {
      for (const operation of syncQueue) {
        try {
          await operation.execute();
        } catch (error) {
          console.error('Ошибка синхронизации:', error);
        }
      }
      setSyncQueue([]);
      localStorage.removeItem('syncQueue');
    };

    syncAll();
  }, [isOnline]);

  return { queueOperation, pendingOperations: syncQueue.length };
}

8. Offline UI компонент

// components/OfflineIndicator.tsx
import { useOnlineStatus } from '@/hooks/useOnlineStatus';

export function OfflineIndicator() {
  const isOnline = useOnlineStatus();

  if (isOnline) return null;

  return (
    <div className='fixed bottom-4 left-4 bg-yellow-100 border-2 border-yellow-400 rounded-lg p-3'>
      <p className='text-sm font-medium'>
        Вы в режиме offline
      </p>
      <p className='text-xs text-content-secondary'>
        Данные загружаются из кеша
      </p>
    </div>
  );
}

9. Next.js конфигурация для PWA

// next.config.js
const withPWA = require('next-pwa');

module.exports = withPWA({
  pwa: {
    dest: 'public',
    register: true,
    skipWaiting: true,
    runtimeCaching: [
      {
        urlPattern: /^https:\/\/api\.example\.com\/.*/i,
        handler: 'NetworkFirst',
        options: {
          cacheName: 'api-cache',
          networkTimeoutSeconds: 10,
          expiration: {
            maxEntries: 50,
            maxAgeSeconds: 5 * 60 // 5 минут
          }
        }
      }
    ]
  }
});

10. Лучшие практики для Offline режима

1. Service Workers для кеширования статики
2. Network First для API - свежие данные когда доступна сеть
3. Выясни status с navigator.onLine
4. Покажи пользователю что он offline
5. Кеши данные в IndexedDB для большого объёма
6. Синхронизируй изменения когда вернётся online
7. Предусмотри fallback UI для offline состояния
8. Тестируй offline в DevTools (Network -> Offline)

Это делает приложение надёжным и работающим везде, даже без интернета!