С чего начать делать приложение работающее в offline
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Разработка 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. Дополнительные рекомендации
- Используйте App Shell Model — минимальный UI, который загружается мгновенно
- Реализуйте стратегию обновления кэша (stale-while-revalidate)
- Добавьте фоновую синхронизацию через Background Sync API
- Обрабатывайте конфликты данных при синхронизации
- Предусмотрите лимиты хранилища — IndexedDB обычно ограничен 50-80% дискового пространства
- Добавьте визуальные индикаторы состояния сети
- Используйте Push API для уведомлений о новых данных
Начните с минимально рабочей версии: зарегистрируйте Service Worker, кэшируйте основные ресурсы, сохраняйте пользовательские данные локально. Постепенно добавляйте более сложную логику синхронизации и обработки конфликтов. Помните — хорошее offline-приложение должно быть прозрачным для пользователя: он не должен задумываться о том, онлайн он или оффлайн.