В этом году на конференции Full Frontal, оффлайн-приложения были популярной темой. Пол Кинлан сделал отличный доклад «Строим веб-приложения будущего. Завтра, сегодня и вчера» (вот его слайды), в котором он сравнивал ощущения пользователей от работы с 50 популярными мобильными приложениями для iOS и Android с ощущениями от веб-сайтов и приложений.
Стоит ли говорить, что нативные приложения зарекомендовали себя с гораздо лучшей стороны, когда соединение с интернетом было недоступно. Оффлайн-режим — очень важная вещь, и стоит думать о нем с самого начала работы над приложением, а не рассчитывать добавить его потом, когда будет время. Работая над сайтом Rareloop, мы с первого дня помнили об оффлайн-режиме. Мобильные клиенты FormAgent тоже были изначально спроектированы для работы в оффлайне, чтобы пользователь мог продолжать работу в отсутствие интернета и прозрачно синхронизироваться, когда связь появляется. В этой статье я описываю принципы и практики, которые, на мой взгляд, очень помогают разрабатывать такие приложения.
Обратите внимание! Я не рассматриваю вопросы кэширования ресурсов приложения — вы можете использовать App Cache или гибридное решение (вроде PhoneGap), это не принципиально [От переводчика: на Хабре есть подробная статья про особенности работы с Application Cache API]. Это руководство посвящено скорее тому, как спроектировать архитектуру веб-приложения для работы в оффлайн-режиме, а не тому, какие механизмы использовать для его реализации.
Базовые принципы
Максимально отвяжите приложение от сервера
Исторически большую часть работы над веб-страницей брал на себя сервер. Данные хранились в БД, доступ к ним осуществлялся через толстый слой кода на серверном языке врде PHP или Ruby, данные обрабатывались и рендерились в HTML с помощью шаблонов. Большинство современных фреймворков используют архитектуру MVC для разделения этих задач, но вся тяжёлая работа по-прежнему делается на сервере. Хранение, обработка и отображение информации требуют постоянной связи с сервером.
Подход offline first предполагает перемещение всего стека MVC на сторону клиента. На стороне сервера остаётся только лёгкий JSON API для доступа к БД. Благодаря этому серверный код становится намного меньше, проще, и его легче тестировать.
Джеймс Пирс тоже говорил об этом на Full Frontal (слайды), в несколько шутливой форме:
Никаких угловых скобок на линии — только фигурные!
Резюме:
- Убедитесь, что клиентское приложение способно обойтись без сервера, предоставляя минимальный функционал. В крайнем случае — хотя бы сообщение о том, что данные не доступны.
- Используйте JSON.
Создайте объект-обёртку для серверного API на стороне клиента
Не загрязняйте код приложения вызовами AJAX с вложенными колбэками. Создайте объект, который будет представлять функциональность сервера внутри приложения. Это способствует разделению кода и облегчает тестирование и отладку, позволяет использовать удобные заглушки на месте ещё не реализованных серверных функций. Внутри этот объект может использовать AJAX, но с точки зрения остального приложения не должно быть видно, как именно он связывается с сервером.
Резюме:
- Абстрагируйте JSON API в отдельном объекте.
- Не засоряйте код приложения вызовами AJAX.
Отвяжите обновление данных от хранилища данных
Не стоит поддаваться искушению просто запрашивать данные напрямую у объекта, абстрагирующего API сервера, и сразу использовать их для рендеринга шаблонов. Лучше создайте объект данных, который будет служить прокси между объектом API и остальным приложением. Этот объект данных будет отвечать за запросы обновлений данных и обрабатывать ситуации, когда связь обрывается — синхронизировать данные, изменённые во время работы в оффлайне.
Объект данных может опрашивать сервер на предмет наличия обновлений, когда пользователь нажмёт кнопку «обновить», или по таймеру, или по событию браузера "online
" — как угодно, а отсутствие прямых обращений к серверу позволяет легче управлять кэшированием данных.
Объект данных также должен отвечать за сериализацию и сохранение своего состояния в постоянном хранилище, в Local Storage или WebSQL/IndexedDB, и уметь восстанавливать эти данные.
Резюме:
- Используйте отдельный объект данных для хранения и синхронизации состояния.
- Вся работа с данными должна идти через это прокси-объект.
Пример
В качестве простого пример возьмём приложение для управления контактами. Сначала сделаем серверный 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