Реализация оффлайн-режима десктоп-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация оффлайн-режима десктоп-приложения
Сложная
~3-5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация оффлайн-режима десктоп-приложения

Оффлайн-режим в десктоп-приложении — это не просто «показать заглушку при отсутствии сети». Это архитектурное решение: приложение должно работать полноценно без сети и синхронизироваться, когда она появится. Между этими состояниями — очередь операций, разрешение конфликтов и честное отображение статуса данных.

Детектирование состояния сети

В Electron из main process:

// main/network-monitor.js
const { net } = require('electron');

class NetworkMonitor {
  constructor() {
    this.isOnline = true;
    this.listeners = new Set();
    this.checkInterval = null;
  }

  start(mainWindow) {
    this.window = mainWindow;

    // Встроенная проверка Electron (не надёжна — показывает наличие интерфейса, не интернета)
    net.online; // true/false

    // Реальная проверка через HTTP
    this.checkInterval = setInterval(() => this.checkConnectivity(), 15000);
    this.checkConnectivity();
  }

  async checkConnectivity() {
    const wasOnline = this.isOnline;

    try {
      // Ping к надёжному эндпоинту с коротким таймаутом
      const response = await Promise.race([
        fetch('https://connectivity-check.your-api.com/ping', { method: 'HEAD' }),
        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))
      ]);
      this.isOnline = response.ok;
    } catch {
      this.isOnline = false;
    }

    if (wasOnline !== this.isOnline) {
      this.window?.webContents.send('network:change', { isOnline: this.isOnline });
      this.emit('change', this.isOnline);
    }
  }

  on(event, listener) {
    this.listeners.add({ event, listener });
  }

  emit(event, data) {
    this.listeners.forEach(l => {
      if (l.event === event) l.listener(data);
    });
  }

  stop() {
    clearInterval(this.checkInterval);
  }
}

module.exports = new NetworkMonitor();

Локальная база данных: SQLite через better-sqlite3

Основа оффлайн-режима — локальное хранилище. SQLite — лучший выбор для структурированных данных:

// main/db.js
const Database = require('better-sqlite3');
const path = require('path');
const { app } = require('electron');

const dbPath = path.join(app.getPath('userData'), 'app.db');
const db = new Database(dbPath);

// Включаем WAL для лучшей производительности
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');

// Миграции
db.exec(`
  CREATE TABLE IF NOT EXISTS documents (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    updated_at INTEGER NOT NULL,
    server_updated_at INTEGER,
    sync_status TEXT NOT NULL DEFAULT 'synced' -- 'synced' | 'pending' | 'conflict'
  );

  CREATE TABLE IF NOT EXISTS sync_queue (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    operation TEXT NOT NULL, -- 'create' | 'update' | 'delete'
    entity_type TEXT NOT NULL,
    entity_id TEXT NOT NULL,
    payload TEXT NOT NULL,
    created_at INTEGER NOT NULL,
    attempts INTEGER NOT NULL DEFAULT 0,
    last_error TEXT
  );

  CREATE INDEX IF NOT EXISTS idx_sync_queue_status ON sync_queue(attempts, created_at);
  CREATE INDEX IF NOT EXISTS idx_documents_sync ON documents(sync_status);
`);

module.exports = db;

Операции с оффлайн-поддержкой

Паттерн «оптимистичное обновление» — записываем локально сразу, синхронизируем потом:

// main/documents.js
const db = require('./db');

function createDocument(doc) {
  const id = doc.id || crypto.randomUUID();
  const now = Date.now();

  // Записываем локально немедленно
  db.prepare(`
    INSERT INTO documents (id, title, content, updated_at, sync_status)
    VALUES (@id, @title, @content, @updated_at, @sync_status)
  `).run({
    id,
    title: doc.title,
    content: doc.content,
    updated_at: now,
    sync_status: 'pending'
  });

  // Добавляем в очередь синхронизации
  db.prepare(`
    INSERT INTO sync_queue (operation, entity_type, entity_id, payload, created_at)
    VALUES (@operation, @entity_type, @entity_id, @payload, @created_at)
  `).run({
    operation: 'create',
    entity_type: 'document',
    entity_id: id,
    payload: JSON.stringify({ id, title: doc.title, content: doc.content }),
    created_at: now
  });

  return { id, title: doc.title, content: doc.content, sync_status: 'pending' };
}

function updateDocument(id, changes) {
  const now = Date.now();

  db.prepare(`
    UPDATE documents
    SET title = COALESCE(@title, title),
        content = COALESCE(@content, content),
        updated_at = @updated_at,
        sync_status = 'pending'
    WHERE id = @id
  `).run({ ...changes, id, updated_at: now });

  db.prepare(`
    INSERT INTO sync_queue (operation, entity_type, entity_id, payload, created_at)
    VALUES ('update', 'document', @id, @payload, @created_at)
  `).run({
    id,
    payload: JSON.stringify({ id, ...changes }),
    created_at: now
  });
}

module.exports = { createDocument, updateDocument };

Процесс синхронизации

// main/sync.js
const db = require('./db');
const networkMonitor = require('./network-monitor');

class SyncManager {
  constructor() {
    this.isSyncing = false;
    networkMonitor.on('change', (isOnline) => {
      if (isOnline) this.sync();
    });
  }

  async sync() {
    if (this.isSyncing || !networkMonitor.isOnline) return;
    this.isSyncing = true;

    try {
      await this.pushPendingOperations();
      await this.pullRemoteChanges();
    } finally {
      this.isSyncing = false;
    }
  }

  async pushPendingOperations() {
    const pending = db.prepare(`
      SELECT * FROM sync_queue
      WHERE attempts < 3
      ORDER BY created_at ASC
      LIMIT 50
    `).all();

    for (const item of pending) {
      try {
        await this.executeOperation(item);

        // Успех — удаляем из очереди
        db.prepare('DELETE FROM sync_queue WHERE id = ?').run(item.id);

        // Обновляем статус документа
        db.prepare(`
          UPDATE documents SET sync_status = 'synced'
          WHERE id = ? AND sync_status = 'pending'
        `).run(item.entity_id);

      } catch (error) {
        // Неудача — увеличиваем счётчик попыток
        db.prepare(`
          UPDATE sync_queue
          SET attempts = attempts + 1, last_error = ?
          WHERE id = ?
        `).run(error.message, item.id);
      }
    }
  }

  async executeOperation(item) {
    const { getAuthToken } = require('./auth');
    const token = await getAuthToken();
    const payload = JSON.parse(item.payload);

    const response = await fetch(`https://api.your-app.com/${item.entity_type}s`, {
      method: item.operation === 'create' ? 'POST'
           : item.operation === 'update' ? 'PUT'
           : 'DELETE',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(payload)
    });

    if (!response.ok) {
      throw new Error(`API error ${response.status}: ${await response.text()}`);
    }

    return response.json();
  }

  async pullRemoteChanges() {
    const { lastSync = 0 } = db.prepare(
      "SELECT value FROM kv_store WHERE key = 'last_sync'"
    ).get() ?? {};

    const response = await fetch(
      `https://api.your-app.com/sync?since=${lastSync}`,
      { headers: { Authorization: `Bearer ${await getAuthToken()}` } }
    );

    if (!response.ok) return;
    const { changes, serverTime } = await response.json();

    // Применяем в транзакции
    db.transaction(() => {
      for (const change of changes) {
        this.applyRemoteChange(change);
      }
      db.prepare("INSERT OR REPLACE INTO kv_store VALUES ('last_sync', ?)").run(serverTime);
    })();
  }

  applyRemoteChange(change) {
    if (change.type === 'document') {
      const local = db.prepare('SELECT * FROM documents WHERE id = ?').get(change.id);

      if (!local) {
        // Новый документ от сервера
        db.prepare(`
          INSERT INTO documents (id, title, content, updated_at, server_updated_at, sync_status)
          VALUES (@id, @title, @content, @updated_at, @server_updated_at, 'synced')
        `).run(change);
      } else if (local.sync_status === 'pending') {
        // Конфликт: у нас есть несинхронизированные изменения
        db.prepare(`
          UPDATE documents SET sync_status = 'conflict', server_updated_at = ?
          WHERE id = ?
        `).run(change.updated_at, change.id);
      } else {
        // Обновляем из сервера
        db.prepare(`
          UPDATE documents
          SET title = ?, content = ?, updated_at = ?, server_updated_at = ?, sync_status = 'synced'
          WHERE id = ?
        `).run(change.title, change.content, change.updated_at, change.updated_at, change.id);
      }
    }
  }
}

module.exports = new SyncManager();

Разрешение конфликтов в UI

// renderer/components/ConflictResolver.tsx
interface ConflictDocument {
  id: string;
  title: string;
  localContent: string;
  serverContent: string;
  localUpdatedAt: number;
  serverUpdatedAt: number;
}

export function ConflictResolver({ doc, onResolve }: { doc: ConflictDocument; onResolve: (choice: 'local' | 'server') => void }) {
  return (
    <div className="conflict-modal">
      <h3>Конфликт синхронизации: {doc.title}</h3>
      <p>Этот документ был изменён и на этом устройстве, и на сервере.</p>

      <div className="conflict-diff">
        <div className="version local">
          <h4>Ваша версия ({new Date(doc.localUpdatedAt).toLocaleString()})</h4>
          <pre>{doc.localContent}</pre>
          <button onClick={() => onResolve('local')}>Оставить мою версию</button>
        </div>
        <div className="version server">
          <h4>Серверная версия ({new Date(doc.serverUpdatedAt).toLocaleString()})</h4>
          <pre>{doc.serverContent}</pre>
          <button onClick={() => onResolve('server')}>Принять серверную версию</button>
        </div>
      </div>
    </div>
  );
}

Индикация статуса синхронизации

Пользователь должен всегда понимать, сохранены ли его данные:

  • Иконка облака с крестиком = нет сети, есть несохранённые изменения
  • Иконка облака со стрелкой = синхронизация идёт
  • Иконка облака с галочкой = всё синхронизировано
  • Иконка предупреждения = есть конфликты

Статус должен быть в постоянно видимой части UI — строка состояния или хедер.

Полная реализация оффлайн-режима с конфликтами, очередью и синхронизацией занимает 3-5 дней разработки только для базового функционала. Сложность растёт с количеством типов сущностей и сложностью бизнес-логики разрешения конфликтов.