Offline-поддержка и синхронизация

Mithril — лёгкий и производительный JavaScript-фреймворк, ориентированный на создание одностраничных приложений. Для реализации offline-поддержки критически важно понимать, как данные хранятся и синхронизируются с сервером. В контексте Mithril ключевую роль играет модель данных и её взаимодействие с локальным хранилищем браузера, таким как localStorage, IndexedDB или сервис-воркеры.

Offline-поддержка обычно включает три компонента:

  1. Кэширование ресурсов приложения — HTML, JS, CSS и статические файлы.
  2. Хранение данных на клиенте — сохранение состояния, которое может быть использовано при отсутствии сети.
  3. Синхронизация с сервером — обновление удалённых данных при восстановлении соединения.

Кэширование ресурсов

Для кеширования статических ресурсов чаще всего используют Service Worker API. В Mithril структура приложения позволяет легко интегрировать сервис-воркер:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service Worker зарегистрирован');
    });
}

Внутри sw.js описывается логика кэширования:

const CACHE_NAME = 'app-cache-v1';
const urlsToCache = [
    '/',
    '/index.html',
    '/app.js',
    '/style.css'
];

self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache))
    );
});

self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request)
              .then(response => response || fetch(event.request))
    );
});

Такой подход обеспечивает доступность приложения даже при отсутствии интернет-соединения.

Хранение данных на клиенте

Mithril использует реактивные модели через функции и объекты, что упрощает интеграцию с локальными хранилищами. Для offline-хранения чаще всего применяются:

  • localStorage — простая синхронная запись данных, подходит для небольших объёмов.
  • IndexedDB — асинхронное хранилище для структурированных данных, подходящее для сложных объектов и больших массивов.

Пример использования localStorage с моделью Mithril:

const TodoModel = {
    list: m.prop(JSON.parse(localStorage.getItem('todos') || '[]')),
    
    add(todo) {
        const current = this.list();
        current.push(todo);
        this.list(current);
        localStorage.setItem('todos', JSON.stringify(current));
    }
};

Для IndexedDB рекомендуется использовать промисифицированные обёртки, такие как idb. Это позволяет хранить и извлекать данные асинхронно, без блокировки UI.

Синхронизация данных

Синхронизация данных между локальным хранилищем и сервером может выполняться по событию online или через периодические запросы. Mithril предоставляет удобный метод m.request, который поддерживает промисы и легко интегрируется с локальными моделями:

function syncTodos() {
    const todos = TodoModel.list();
    m.request({
        method: 'POST',
        url: '/api/todos/sync',
        body: todos
    }).then(serverResponse => {
        // Обновление локального хранилища на основе ответа сервера
        TodoModel.list(serverResponse);
        localStorage.setItem('todos', JSON.stringify(serverResponse));
    }).catch(err => {
        console.error('Синхронизация не удалась', err);
    });
}

window.addEventListener('online', syncTodos);

Ключевой принцип — двусторонняя синхронизация: изменения в локальном хранилище должны отправляться на сервер, а серверные обновления — сохраняться локально. Для этого применяются методы слияния данных, проверка временных меток и версий объектов.

Обработка конфликтов данных

При offline-режиме возможны конфликты, если одни и те же данные изменяются на клиенте и сервере. Стратегии разрешения конфликтов:

  • Последнее изменение побеждает (Last-Write-Wins) — простая, но рискованная стратегия.
  • Мерж по полям — применимо к сложным объектам, объединяет изменения из разных источников.
  • Пользовательское разрешение — вывод диалогового окна для выбора версии данных.

Пример простого слияния по полям для задач:

function mergeTodos(localTodos, serverTodos) {
    const merged = [...serverTodos];
    localTodos.forEach(local => {
        const index = merged.findIndex(s => s.id === local.id);
        if (index !== -1) {
            merged[index] = { ...merged[index], ...local };
        } else {
            merged.push(local);
        }
    });
    return merged;
}

Реактивное обновление интерфейса

Mithril автоматически обновляет представление при изменении данных модели. Для offline-поддержки это важно, чтобы изменения локальных данных сразу отображались пользователю:

m.mount(document.body, {
    view() {
        return m('ul', TodoModel.list().map(todo => m('li', todo.text)));
    }
});

Каждое добавление или синхронизация данных вызывает обновление TodoModel.list(), и интерфейс перерисовывается автоматически.

Практические рекомендации

  • Использовать сервис-воркеры для кеширования статических файлов и API-запросов.
  • Хранить критически важные данные в IndexedDB для масштабируемости.
  • Обрабатывать событие online для немедленной синхронизации.
  • Разрабатывать стратегию разрешения конфликтов заранее.
  • Поддерживать реактивность модели для мгновенного обновления интерфейса при изменении данных.

Offline-поддержка и синхронизация в Mithril строятся вокруг моделей, сервис-воркеров и локального хранилища, обеспечивая быстрый и стабильный интерфейс независимо от состояния сети.