Offline functionality и sync strategies

Backbone.js изначально проектировался как тонкий MVC-фреймворк для браузера с опорой на REST-синхронизацию. Однако архитектура Backbone допускает расширение и переопределение механизмов синхронизации, что делает возможной полноценную offline-first стратегию.

Ключевым элементом является метод Backbone.sync, который отвечает за обмен данными между моделями/коллекциями и сервером. Любая offline-функциональность в Backbone строится вокруг кастомизации этого метода и использования локальных хранилищ браузера.

Основные варианты локального хранения:

  • localStorage
  • sessionStorage
  • IndexedDB
  • WebSQL (устаревший, но встречающийся в старых проектах)
  • In-memory storage (для временного offline-состояния)

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


Переопределение Backbone.sync

Backbone.sync принимает пять параметров:

Backbone.sync = function(method, model, options) {
    // method: 'create', 'read', 'update', 'delete'
    // model: Backbone.Model или Backbone.Collection
    // options: success, error и др.
};

Для offline-режима стратегия обычно делится на два уровня:

  1. Локальный слой — чтение и запись данных без сети
  2. Сетевой слой — отложенная синхронизация с сервером

Простейший пример подмены sync для localStorage:

Backbone.sync = function(method, model, options) {
    var key = model.urlRoot || model.url;
    var data = JSON.parse(localStorage.getItem(key)) || [];

    switch (method) {
        case 'read':
            options.success(data);
            break;
        case 'create':
            data.push(model.toJSON());
            localStorage.setItem(key, JSON.stringify(data));
            options.success(model.toJSON());
            break;
    }
};

Такой подход демонстрирует принцип, но не решает задачи идентификации моделей, конфликтов и очередей изменений.


Backbone.LocalStorage как базовая реализация

Популярным решением является использование плагина Backbone.LocalStorage, который реализует полноценный storage-адаптер.

Пример подключения:

var Todo = Backbone.Model.extend({
    defaults: {
        title: '',
        completed: false
    }
});

var Todos = Backbone.Collection.extend({
    model: Todo,
    localStorage: new Backbone.LocalStorage('todos-backbone')
});

Плагин автоматически переопределяет sync для конкретной коллекции или модели, сохраняя данные в localStorage с поддержкой CRUD-операций.

Преимущества подхода:

  • минимальная настройка
  • прозрачная работа с моделями
  • отсутствие изменений в остальном коде

Ограничения:

  • ограничение объёма localStorage
  • отсутствие транзакций
  • отсутствие встроенной синхронизации с сервером

Offline-first архитектура

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

Основные принципы:

  • Все операции записи происходят локально
  • Серверная синхронизация асинхронна
  • UI не зависит от доступности сети
  • Конфликты разрешаются детерминированно

В Backbone это реализуется через разделение логики:

  • Model — хранит состояние и флаги синхронизации
  • Collection — управляет очередями изменений
  • Sync layer — решает, когда и как отправлять данные

Очереди изменений (Change Queue)

Для поддержки offline-режима необходимо хранить историю операций, которые ещё не были отправлены на сервер.

Пример структуры очереди:

[
  { "method": "create", "model": {...}, "timestamp": 123456 },
  { "method": "update", "model": {...}, "timestamp": 123789 }
]

Очередь может храниться в localStorage или IndexedDB. Backbone-модели дополняются метаданными:

var Task = Backbone.Model.extend({
    defaults: {
        synced: false,
        updatedAt: null
    }
});

При каждом save в offline-режиме:

  • данные сохраняются локально
  • операция добавляется в очередь
  • модель помечается как synced: false

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

1. Last Write Wins (LWW)

Самая простая стратегия. Последнее изменение перезаписывает предыдущие.

Особенности:

  • минимальная сложность
  • возможная потеря данных
  • подходит для некритичных данных

Реализация обычно основана на поле updatedAt.

2. Client Wins / Server Wins

  • Client Wins — локальные изменения всегда имеют приоритет
  • Server Wins — серверные данные перезаписывают локальные

Выбор стратегии зависит от бизнес-логики. В Backbone это реализуется на уровне обработки ответа сервера в success колбэке.

3. Версионирование моделей

Каждая модель содержит версию:

defaults: {
    version: 1
}

При синхронизации сервер сравнивает версии и возвращает конфликт, если данные устарели.

Backbone позволяет перехватывать такие ответы и вручную разрешать конфликт, например, через:

model.fetch({
    error: function(model, response) {
        if (response.status === 409) {
            // конфликт версий
        }
    }
});

Гибридный sync: локально + сервер

Часто используется комбинированный sync, который:

  1. Сначала работает с локальным хранилищем
  2. При наличии сети — отправляет данные на сервер
  3. При успехе — обновляет локальное состояние

Пример архитектурного паттерна:

var originalSync = Backbone.sync;

Backbone.sync = function(method, model, options) {
    localSync(method, model, options);

    if (navigator.onLine) {
        return originalSync(method, model, {
            success: function(resp) {
                markAsSynced(model);
                options.success(resp);
            }
        });
    }
};

Такой подход позволяет сохранить стандартный REST-механизм Backbone, добавив offline-уровень.


Использование IndexedDB для масштабируемости

localStorage синхронен и ограничен по объёму. Для сложных приложений предпочтительнее IndexedDB.

Основные преимущества:

  • асинхронность
  • большие объёмы данных
  • индексы и запросы

В Backbone IndexedDB обычно используется через адаптеры или кастомный sync.

Пример концепции:

function indexedDBSync(method, model, options) {
    // асинхронная работа с IndexedDB
}

Такой sync может работать параллельно с сетевым, формируя надёжную offline-first модель.


Отслеживание состояния сети

Для корректной синхронизации важно отслеживать доступность сети:

window.addEventListener('online', syncQueue);
window.addEventListener('offline', markOffline);

Backbone-приложение может хранить глобальное состояние:

var AppState = new Backbone.Model({
    online: navigator.onLine
});

Это состояние используется для:

  • блокировки сетевых запросов
  • отображения статуса синхронизации
  • управления очередями изменений

UI и offline-состояние моделей

Backbone.Views должны учитывать offline-статус модели:

  • индикаторы несинхронизированных данных
  • отключение необратимых действий
  • визуальные маркеры конфликтов

Типичный паттерн:

model.on('change:synced', this.render, this);

Таким образом UI остаётся реактивным и не зависит от немедленного ответа сервера.


Итоговая архитектурная схема

  • Backbone.Model — хранит данные и метаданные синхронизации
  • Backbone.Collection — управляет наборами моделей и очередями
  • Custom Backbone.sync — маршрутизирует операции
  • Local storage / IndexedDB — основной источник данных
  • Server API — вторичный источник и точка согласования

Backbone.js, несмотря на возраст, предоставляет достаточно гибкие примитивы для реализации сложных offline-first стратегий без нарушения основной архитектуры фреймворка.