← Назад к вопросам
Что будешь использовать для взаимодействия приложения с offline режимом?
2.0 Middle🔥 131 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Взаимодействие приложения с offline режимом
Построение приложения, которое работает в offline режиме - это критически важный навык для современного фронтенд-разработчика. Расскажу о всех необходимых технологиях и подходах.
1. Определение онлайн/офлайн статуса
Первый шаг - определять, когда приложение потеряло интернет.
// Базовая проверка
const isOnline = navigator.onLine;
console.log(isOnline); // true или false
// События изменения статуса
window.addEventListener('online', () => {
console.log('Вернулось в онлайн');
syncData(); // Синхронизировать накопленные изменения
});
window.addEventListener('offline', () => {
console.log('Потеря интернета');
enableOfflineMode(); // Переключиться в офлайн-режим
});
// React Hook для отслеживания статуса
import { useEffect, useState } from 'react';
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
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;
}
// Использование
export function App() {
const isOnline = useOnlineStatus();
return (
<div>
{!isOnline && <div className="offline-banner">Вы офлайн</div>}
{isOnline && <div className="online-banner">Онлайн</div>}
</div>
);
}
2. Service Workers (ОСНОВНАЯ ТЕХНОЛОГИЯ)
Service Worker - это скрипт, который работает в отдельном потоке и может перехватывать сетевые запросы.
// В главном файле приложения
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('Service Worker зарегистрирован'))
.catch(err => console.error('Ошибка регистрации:', err));
}
// sw.js - Service Worker скрипт
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/index.html',
'/style.css',
'/app.js',
'/offline.html'
];
// Событие установки
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// Событие активации
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
// Перехват запросов
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// Только для GET запросов
if (request.method !== 'GET') {
return;
}
// Стратегия: Cache first, then network
event.respondWith(
caches.match(request)
.then(response => {
if (response) {
return response;
}
return fetch(request).then(response => {
// Кешировать только успешные ответы
if (!response || response.status !== 200) {
return response;
}
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(request, responseToCache));
return response;
});
})
.catch(() => caches.match('/offline.html'))
);
});
3. Стратегии кеширования
// Стратегия 1: Cache First (для статических ресурсов)
const cacheFirst = (request) => {
return caches.match(request)
.then(response => response || fetch(request))
.catch(() => caches.match('/offline.html'));
};
// Стратегия 2: Network First (для данных)
const networkFirst = (request) => {
return fetch(request)
.then(response => {
if (response.status === 200) {
caches.open(CACHE_NAME).then(cache => {
cache.put(request, response.clone());
});
}
return response;
})
.catch(() => caches.match(request));
};
// Стратегия 3: Stale While Revalidate (быстрый ответ + обновление)
const staleWhileRevalidate = (request) => {
return caches.match(request)
.then(cachedResponse => {
const fetchPromise = fetch(request).then(response => {
caches.open(CACHE_NAME).then(cache => {
cache.put(request, response.clone());
});
return response;
});
return cachedResponse || fetchPromise;
})
.catch(() => caches.match('/offline.html'));
};
4. Локальное хранилище для данных
// IndexedDB - для больших объемов данных
class OfflineDB {
constructor(dbName = 'AppDB') {
this.dbName = dbName;
this.db = null;
}
init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Создать хранилища объектов
if (!db.objectStoreNames.contains('posts')) {
db.createObjectStore('posts', { keyPath: 'id' });
}
if (!db.objectStoreNames.contains('pendingRequests')) {
db.createObjectStore('pendingRequests', { keyPath: 'id', autoIncrement: true });
}
};
});
}
// Сохранить объект
save(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// Получить объект
get(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName]);
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Получить все объекты
getAll(storeName) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName]);
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// Использование
const db = new OfflineDB();
await db.init();
await db.save('posts', { id: 1, title: 'Post 1', content: 'Content' });
const post = await db.get('posts', 1);
5. Синхронизация при возврате онлайн
class SyncManager {
constructor(db) {
this.db = db;
}
// Сохранить запрос для последующей синхронизации
async savePendingRequest(method, url, data) {
await this.db.save('pendingRequests', {
method,
url,
data,
timestamp: Date.now()
});
}
// Синхронизировать все ожидающие запросы
async syncPendingRequests() {
const pendingRequests = await this.db.getAll('pendingRequests');
for (const request of pendingRequests) {
try {
const response = await fetch(request.url, {
method: request.method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request.data)
});
if (response.ok) {
// Удалить из очереди
await this.db.delete('pendingRequests', request.id);
console.log('Синхронизирован:', request.url);
}
} catch (error) {
console.error('Ошибка синхронизации:', error);
}
}
}
}
// Использование
window.addEventListener('online', async () => {
const syncManager = new SyncManager(db);
await syncManager.syncPendingRequests();
});
6. Практический пример: Офлайн-приложение
import { useOnlineStatus } from './hooks/useOnlineStatus';
import { useOfflineDB } from './hooks/useOfflineDB';
export function OfflineApp() {
const isOnline = useOnlineStatus();
const db = useOfflineDB();
const [posts, setPosts] = useState([]);
// Загрузить посты
const loadPosts = async () => {
if (isOnline) {
// Загрузить с сервера
const response = await fetch('/api/posts');
const data = await response.json();
setPosts(data);
// Сохранить в IndexedDB
data.forEach(post => db.save('posts', post));
} else {
// Загрузить из IndexedDB
const cachedPosts = await db.getAll('posts');
setPosts(cachedPosts);
}
};
// Создать пост
const createPost = async (title, content) => {
const newPost = { id: Date.now(), title, content };
if (isOnline) {
// Отправить на сервер
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost)
});
} else {
// Сохранить в очередь
await db.save('pendingRequests', {
method: 'POST',
url: '/api/posts',
data: newPost
});
}
// В обоих случаях сохранить локально
await db.save('posts', newPost);
setPosts([...posts, newPost]);
};
return (
<div>
{!isOnline && <div className="warning">Вы офлайн</div>}
<button onClick={() => loadPosts()}>Загрузить</button>
<button onClick={() => createPost('New', 'Content')}>Создать</button>
<div>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>
</div>
);
}
7. Стек технологий для офлайна
const offlineTechStack = {
'Определение статуса': 'navigator.onLine + online/offline события',
'Кеширование': 'Service Workers + Cache API',
'Локальное хранилище': 'IndexedDB для больших данных, localStorage для настроек',
'Синхронизация': 'Background Sync API, Background Fetch',
'Работа с данными': 'Optimistic updates, sync queue',
'UI индикаторы': 'React Context для статуса онлайна',
'Testing': 'Mock Service Worker, DevTools offline режим'
};
Итог
Для офлайн-функциональности нужна комбинация технологий: Service Workers для кеширования, IndexedDB для данных, Background Sync для синхронизации. Главный принцип - работать локально в офлайне и синхронизировать при возврате онлайн.