← Назад к вопросам
Как реализована поддержка 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)
Это делает приложение надёжным и работающим везде, даже без интернета!