«Offline first» подход к созданию веб-приложений

в 18:58, , рубрики: application cache, javascript, offline first, Веб-разработка, Мобильный веб, оффлайн-приложения

В этом году на конференции Full Frontal, оффлайн-приложения были популярной темой. Пол Кинлан сделал отличный доклад «Строим веб-приложения будущего. Завтра, сегодня и вчера» (вот его слайды), в котором он сравнивал ощущения пользователей от работы с 50 популярными мобильными приложениями для iOS и Android с ощущениями от веб-сайтов и приложений.

Стоит ли говорить, что нативные приложения зарекомендовали себя с гораздо лучшей стороны, когда соединение с интернетом было недоступно. Оффлайн-режим — очень важная вещь, и стоит думать о нем с самого начала работы над приложением, а не рассчитывать добавить его потом, когда будет время. Работая над сайтом Rareloop, мы с первого дня помнили об оффлайн-режиме. Мобильные клиенты FormAgent тоже были изначально спроектированы для работы в оффлайне, чтобы пользователь мог продолжать работу в отсутствие интернета и прозрачно синхронизироваться, когда связь появляется. В этой статье я описываю принципы и практики, которые, на мой взгляд, очень помогают разрабатывать такие приложения.

Обратите внимание! Я не рассматриваю вопросы кэширования ресурсов приложения — вы можете использовать App Cache или гибридное решение (вроде PhoneGap), это не принципиально [От переводчика: на Хабре есть подробная статья про особенности работы с Application Cache API]. Это руководство посвящено скорее тому, как спроектировать архитектуру веб-приложения для работы в оффлайн-режиме, а не тому, какие механизмы использовать для его реализации.

Базовые принципы

Максимально отвяжите приложение от сервера

Исторически большую часть работы над веб-страницей брал на себя сервер. Данные хранились в БД, доступ к ним осуществлялся через толстый слой кода на серверном языке врде PHP или Ruby, данные обрабатывались и рендерились в HTML с помощью шаблонов. Большинство современных фреймворков используют архитектуру MVC для разделения этих задач, но вся тяжёлая работа по-прежнему делается на сервере. Хранение, обработка и отображение информации требуют постоянной связи с сервером.

Подход offline first предполагает перемещение всего стека MVC на сторону клиента. На стороне сервера остаётся только лёгкий JSON API для доступа к БД. Благодаря этому серверный код становится намного меньше, проще, и его легче тестировать.

Джеймс Пирс тоже говорил об этом на Full Frontal (слайды), в несколько шутливой форме:

Никаких угловых скобок на линии — только фигурные!

Резюме:

  1. Убедитесь, что клиентское приложение способно обойтись без сервера, предоставляя минимальный функционал. В крайнем случае — хотя бы сообщение о том, что данные не доступны.
  2. Используйте JSON.
Создайте объект-обёртку для серверного API на стороне клиента

Не загрязняйте код приложения вызовами AJAX с вложенными колбэками. Создайте объект, который будет представлять функциональность сервера внутри приложения. Это способствует разделению кода и облегчает тестирование и отладку, позволяет использовать удобные заглушки на месте ещё не реализованных серверных функций. Внутри этот объект может использовать AJAX, но с точки зрения остального приложения не должно быть видно, как именно он связывается с сервером.

Резюме:

  1. Абстрагируйте JSON API в отдельном объекте.
  2. Не засоряйте код приложения вызовами AJAX.

Отвяжите обновление данных от хранилища данных

Не стоит поддаваться искушению просто запрашивать данные напрямую у объекта, абстрагирующего API сервера, и сразу использовать их для рендеринга шаблонов. Лучше создайте объект данных, который будет служить прокси между объектом API и остальным приложением. Этот объект данных будет отвечать за запросы обновлений данных и обрабатывать ситуации, когда связь обрывается — синхронизировать данные, изменённые во время работы в оффлайне.

Объект данных может опрашивать сервер на предмет наличия обновлений, когда пользователь нажмёт кнопку «обновить», или по таймеру, или по событию браузера "online" — как угодно, а отсутствие прямых обращений к серверу позволяет легче управлять кэшированием данных.

Объект данных также должен отвечать за сериализацию и сохранение своего состояния в постоянном хранилище, в Local Storage или WebSQL/IndexedDB, и уметь восстанавливать эти данные.

Резюме:

  1. Используйте отдельный объект данных для хранения и синхронизации состояния.
  2. Вся работа с данными должна идти через это прокси-объект.

Пример

В качестве простого пример возьмём приложение для управления контактами. Сначала сделаем серверный API, который позволит нам получать сырые данные кнтактов. Предположим, что мы создали RESTful API, где URI /contacts возвращает список всех записей контактов. В каждой записи есть поля id, firstName, lastName и email.

Затем напишем объект-обёртку над этим API:

var API = function() { };

API.prototype.getContacts = function(success, failure) {
    var win = function(data) {
        if(success)
            success(data);
    };

    var fail = function() {
        if(failure)
            failure()
    };

    $.ajax('http://myserver.com/contacts', {
        success: win,
        failure: fail
    });
};

Теперь нам нужен объект данных, который станет интерфейсом между нашим приложением и хранилищем данных. Он может выглядеть так:

var Data = function() {
    this.api = new API();
    this.contacts = this.readFromStorage();
    this.indexData();
};

Data.prototype.indexData = function() {
    // Выполняем индексирование (например, по email)
};

/* -- API апдейтов-- */

Data.prototype.updateFromServer = function(callback) {
    var _this = this;

    var win = function(data) {
        _this.contacts = data;
        _this.indexData();

        if(callback)
            callback();
    };

    var fail = function() {
        if(callback)
            callback();
    };

    this.api.getContacts(win, fail);
};

/* -- Сериализация данных -- */

Data.prototype.readFromStorage = function() {
    var c = JSON.parse(window.localStorage.getItem('appData'));

    // позаботимся о результате по умолчанию
    return c || [];
};

Data.prototype.writeToStorage = function() {
    window.localStorage.setItem('appData', JSON.stringify(this.contacts));
};

/* -- Стандартные геттеры/сеттеры -- */

Data.prototype.getContacts = function() {
    return this.contacts;
};

// Запрос данных, специфичный для приложения
Data.prototype.getContactWithEmail = function(email) {
    // Поиск контактов с помощью механизмов индексирования...
    return contact;
};

Теперь у нас есть объект данных, через который мы можем запросить обновление у серверного API, но по умолчанию он будет возвращать данные, сохранённые локально. В остальном приложении можно использовать вот такой код:

var App = function() {
    this.data = new Data();
    this.template = '...';

    this.render();
    this.setupListeners();
};

App.prototype.render = function() {
    // Используем this.template и this.data.getContacts() для рендеринга HTML
    return html;
}

App.prototype.setupListeners = function() {
    var _this = this;

    // Обновляем данные с сервера
    $('button.refresh').on('click', function(event) {
        _this.refresh();
    });
};

App.prototype.refresh = function () {
    _this.showLoadingSpinner();

    _this.data.updateFromServer(function() {
        // Данные пришли с сервера
        _this.render();
        _this.hideLoadingSpinner();
    });
};

App.prototype.showLoadingSpinner = function() {
    // показываем крутилку
};

App.prototype.hideLoadingSpinner = function() {
    // прячем крутилку
};

Это очень простой пример, в реальном приложении, вы, вероятно, захотите реализовать объект данных, как singleton, но для илююстрации того, как надо структурировать код для работы в режиме оффлайн этого достаточно.

Автор: ilya42

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js