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

Что будешь использовать для взаимодействия приложения с 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 для синхронизации. Главный принцип - работать локально в офлайне и синхронизировать при возврате онлайн.

Что будешь использовать для взаимодействия приложения с offline режимом? | PrepBro