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

С чего начать делать приложение работающее в offline

1.0 Junior🔥 211 комментариев
#Soft Skills и рабочие процессы

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

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

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

Разработка Offline-First приложения: с чего начать

Создание приложения, работающего в offline-режиме, требует особого подхода к архитектуре. Вместо традиционной модели "онлайн с fallback" мы используем offline-first парадигму, где приложение изначально проектируется для работы без сети.

1. Выбор архитектурного подхода

Первым делом определитесь с архитектурой:

  • Кэширование ресурсов: Для статических приложений (документы, блоги)
  • Service Worker + Cache API: Для прогрессивных веб-приложений (PWA)
  • IndexedDB + синхронизация: Для сложных приложений с большими объемами данных
  • Фоновые процессы: Для приложений, требующих периодической синхронизации

2. Установка Service Worker

Service Worker — ключевая технология для offline-работы. Это прокси-скрипт между приложением и сетью.

// sw.js - базовый Service Worker
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/manifest.json'
];

// Установка и кэширование ресурсов
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(STATIC_ASSETS))
      .then(() => self.skipWaiting())
  );
});

// Активация и очистка старых кэшей
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys.filter(key => key !== CACHE_NAME)
          .map(key => caches.delete(key))
      );
    }).then(() => self.clients.claim())
  );
});

// Перехват сетевых запросов
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Возвращаем из кэша или делаем сетевой запрос
        return response || fetch(event.request)
          .then(networkResponse => {
            // Кэшируем динамические ответы
            return caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, networkResponse.clone());
                return networkResponse;
              });
          });
      }).catch(() => {
        // Fallback для offline-режима
        return caches.match('/offline.html');
      })
  );
});

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

// app.js - регистрация Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('ServiceWorker registered:', registration.scope);
    } catch (error) {
      console.error('ServiceWorker registration failed:', error);
    }
  });
}

4. Работа с данными в offline-режиме

Для хранения данных используйте комбинацию технологий:

// Пример работы с IndexedDB через библиотеку idb
import { openDB } from 'idb';

class OfflineStorage {
  constructor() {
    this.dbPromise = this.initDB();
  }

  async initDB() {
    return openDB('app-database', 1, {
      upgrade(db) {
        // Создаем хранилище для пользовательских данных
        if (!db.objectStoreNames.contains('tasks')) {
          const tasksStore = db.createObjectStore('tasks', {
            keyPath: 'id',
            autoIncrement: true
          });
          tasksStore.createIndex('by-status', 'status');
          tasksStore.createIndex('by-date', 'createdAt');
        }

        // Создаем хранилище для синхронизации
        if (!db.objectStoreNames.contains('syncQueue')) {
          const syncStore = db.createObjectStore('syncQueue', {
            keyPath: 'id',
            autoIncrement: true
          });
          syncStore.createIndex('by-type', 'type');
          syncStore.createIndex('by-status', 'syncStatus');
        }
      }
    });
  }

  async saveTask(task) {
    const db = await this.dbPromise;
    const tx = db.transaction('tasks', 'readwrite');
    const store = tx.objectStore('tasks');
    
    // Сохраняем локально
    const id = await store.add({
      ...task,
      syncStatus: 'pending',
      createdAt: new Date().toISOString()
    });
    
    // Добавляем в очередь синхронизации
    await this.addToSyncQueue('task', { ...task, localId: id });
    
    return id;
  }

  async addToSyncQueue(type, data) {
    const db = await this.dbPromise;
    const tx = db.transaction('syncQueue', 'readwrite');
    await tx.objectStore('syncQueue').add({
      type,
      data,
      syncStatus: 'pending',
      createdAt: new Date().toISOString()
    });
  }
}

5. Обнаружение состояния сети

// network-manager.js - управление состоянием сети
class NetworkManager {
  constructor() {
    this.online = navigator.onLine;
    this.listeners = [];
    
    this.setupEventListeners();
  }

  setupEventListeners() {
    window.addEventListener('online', () => {
      this.online = true;
      this.notifyListeners();
      this.triggerSync();
    });

    window.addEventListener('offline', () => {
      this.online = false;
      this.notifyListeners();
      this.showOfflineNotification();
    });
  }

  async triggerSync() {
    if (!this.online) return;
    
    const syncManager = new SyncManager();
    await syncManager.processQueue();
    
    // Обновляем данные с сервера
    await this.fetchUpdates();
  }

  async fetchUpdates() {
    try {
      const response = await fetch('/api/sync', {
        headers: {
          'If-Modified-Since': localStorage.getItem('lastSync') || ''
        }
      });
      
      if (response.ok) {
        const updates = await response.json();
        await this.applyUpdates(updates);
        localStorage.setItem('lastSync', new Date().toISOString());
      }
    } catch (error) {
      console.error('Sync failed:', error);
    }
  }

  addListener(callback) {
    this.listeners.push(callback);
  }

  notifyListeners() {
    this.listeners.forEach(callback => callback(this.online));
  }

  showOfflineNotification() {
    // Показываем пользователю, что приложение работает в offline-режиме
    if ('Notification' in window && Notification.permission === 'granted') {
      new Notification('Приложение перешло в автономный режим');
    }
  }
}

6. Стратегия синхронизации

Реализуйте умную синхронизацию:

// sync-manager.js - управление синхронизацией
class SyncManager {
  constructor() {
    this.isSyncing = false;
    this.retryCount = 0;
    this.maxRetries = 3;
  }

  async processQueue() {
    if (this.isSyncing) return;
    
    this.isSyncing = true;
    
    try {
      const db = await openDB('app-database', 1);
      const tx = db.transaction('syncQueue', 'readonly');
      const queue = await tx.objectStore('syncQueue')
        .index('by-status')
        .getAll(IDBKeyRange.only('pending'));
      
      for (const item of queue) {
        await this.processItem(item);
      }
      
      this.retryCount = 0; // Сбрасываем счетчик при успешной синхронизации
    } catch (error) {
      console.error('Sync error:', error);
      
      // Экспоненциальная задержка для повторных попыток
      if (this.retryCount < this.maxRetries) {
        this.retryCount++;
        const delay = Math.pow(2, this.retryCount) * 1000;
        setTimeout(() => this.processQueue(), delay);
      }
    } finally {
      this.isSyncing = false;
    }
  }

  async processItem(item) {
    try {
      // Отправляем данные на сервер
      const response = await fetch(`/api/${item.type}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(item.data)
      });

      if (response.ok) {
        await this.markAsSynced(item.id);
        
        const result = await response.json();
        await this.updateLocalData(item, result);
      } else {
        throw new Error(`HTTP ${response.status}`);
      }
    } catch (error) {
      await this.markAsFailed(item.id, error.message);
      throw error;
    }
  }

  async markAsSynced(id) {
    const db = await openDB('app-database', 1);
    const tx = db.transaction('syncQueue', 'readwrite');
    await tx.objectStore('syncQueue').put({
      syncStatus: 'synced',
      syncedAt: new Date().toISOString()
    }, id);
  }
}

7. Тестирование offline-режима

Важные аспекты тестирования:

  • Симуляция разных условий сети (Chrome DevTools → Network → Throttling)
  • Тестирование на реальных мобильных устройствах в режиме полета
  • Проверка восстановления после потери соединения
  • Тестирование обработки конфликтов данных
  • Проверка использования памяти и хранилища

8. Дополнительные рекомендации

  1. Используйте App Shell Model — минимальный UI, который загружается мгновенно
  2. Реализуйте стратегию обновления кэша (stale-while-revalidate)
  3. Добавьте фоновую синхронизацию через Background Sync API
  4. Обрабатывайте конфликты данных при синхронизации
  5. Предусмотрите лимиты хранилища — IndexedDB обычно ограничен 50-80% дискового пространства
  6. Добавьте визуальные индикаторы состояния сети
  7. Используйте Push API для уведомлений о новых данных

Начните с минимально рабочей версии: зарегистрируйте Service Worker, кэшируйте основные ресурсы, сохраняйте пользовательские данные локально. Постепенно добавляйте более сложную логику синхронизации и обработки конфликтов. Помните — хорошее offline-приложение должно быть прозрачным для пользователя: он не должен задумываться о том, онлайн он или оффлайн.

С чего начать делать приложение работающее в offline | PrepBro