Оффлайн-приложения становятся все более популярными. Автономная поддержка настолько важна, что в настоящее время принято говорить о подходе «Offline First», когда он становится основным фактором. Он также набирает популярность с ростом Progressive Web Apps.
В этом посте мы рассмотрим, как добавить автономную поддержку в основное веб-приложение со списком контактов путем реализации кэширования ресурсов, хранения данных на стороне клиента и синхронизации с удаленным хранилищем данных.
Исходный код приложения доступен на GitHub.
Зачем нужна поддержка в автономном режиме?
Почему мы должны заботиться о поддержке в автономном режиме?
Я сам провожу больше часа в поезде каждый день. Я не хочу тратить это время впустую, поэтому по дороге беру свой ноутбук с собой. Я использую сотовую сеть, чтобы быть онлайн. Соединение не надежное, поэтому время от времени я его теряю. Мой пользовательский опыт зависит от веб-приложения, которое я использую. Лишь несколько приложений с хорошей автономной поддержкой ведут себя так, как ожидается, и потеря соединения прозрачна. Некоторые ведут себя странно, поэтому, когда я обновляю страницу, я теряю данные. Большинство вообще не поддерживают автономный режим, и мне приходится ждать стабильного соединения, чтобы использовать их.
Ненадежное соединение — не единственный вариант использования. Мы также можем поговорить о ситуациях, когда вы можете быть в автономном режиме в течение нескольких часов, например, находясь в самолете.
Еще одним важным преимуществом автономной поддержки является повышение производительности. Действительно, браузеру не нужно ждать загрузки ресурсов с сервера. То же самое для данных, когда-то хранящихся на клиенте.
Таким образом, нам нужно в автономном режиме:
- иметь возможность использовать приложения даже при нестабильном соединении (сотовая сеть в поезде)
- иметь возможность работать без подключения к сети (на самолете)
- повысить производительность
Прогрессивные веб-приложения
Концепция Google Progressive Web Apps (PWA) — это методология, предназначенная для предоставления веб-приложений, которые предоставляют UX собственных мобильных приложений. PWA включает поддержку в автономном режиме, но она также охватывает гораздо больше:
- Отзывчивость — поддержка разных форм-факторов: мобильный, планшетный, настольный
- Web App Manifest — для установки приложения на домашний экран
- App Shell — шаблон проектирования, в котором базовая оболочка приложения пользовательского интерфейса отделена от содержимого, загруженного впоследствии
- Push-уведомления — чтобы получать «мгновенные» обновления с сервера
Адди Османи написала отличное вступительное сообщение о PWA .
В этой статье мы сосредоточимся только на одном аспекте: автономной поддержке.
Определение автономной поддержки
Давайте уточним, что требуется для поддержки в автономном режиме. Нам нужно позаботиться о двух аспектах:
- ресурсы приложения — кеширование HTML, JS-скрипты, таблицы стилей CSS, изображения
- данные приложения — хранение данных на стороне клиента
Активы приложения
Первым решением в HTML5 для кэширования автономных ресурсов был AppCache . Идея состоит в том, чтобы предоставить манифест приложения, описывающий, какие ресурсы должны храниться в кэше браузера. Таким образом, при следующей загрузке приложения эти ресурсы будут взяты из кэша браузера.
Важно : Несмотря на простоту, существует довольно много подводных камней при использовании AppCache. Стандарт сейчас устарел, хотя все еще широко поддерживается браузерами .
Сервисные работники были введены для замены AppCache. Они предоставляют гибкое решение для автономной поддержки. Сервисные работники дают контроль над исходящими запросами, позволяя сценарию перехватывать их и возвращать необходимые ответы. Логика кэширования полностью лежит на плечах разработчика. Сам код приложения может проверить, сохранен ли актив в кеше, и запрашивает его с сервера только при необходимости.
Важно отметить, что Service Workers поддерживаются только через HTTPS (HTTP разрешен для localhost) соединений. Мы рассмотрим, как использовать Service Workers в ближайшее время.
Данные приложения
Данные приложения могут храниться в автономном хранилище, предоставляемом браузерами.
HTML5 предлагает несколько вариантов:
- WebStorage — хранилище ключ-значение
- IndexedDB — база данных NoSQL
- WebSQL — встроенная база данных SQLite
WebStorage — это хранилище ключей и значений. Это самое простое кросс-браузерное хранилище, но есть несколько подводных камней, о которых следует помнить. Вы должны позаботиться о сериализации и десериализации данных, которые вы помещаете внутрь, потому что значения должны быть простыми строками. Вы можете столкнуться с ограничениями размера с большими наборами данных . Кроме того, можно попасть в состояние гонки, то есть, если в браузере одновременно открыты две вкладки, вы можете столкнуться с неожиданным поведением.
IndexedDB гораздо более мощный и, кажется, лучший способ использовать автономное хранилище. В нем достаточно свободного места . Он поддерживает транзакции и может безопасно использоваться в нескольких вкладках браузера одновременно. Это также поддерживается всеми современными браузерами .
WebSQL — это буквально SQLite в браузере. Полнофункциональная реляционная БД с ACID на клиенте. К сожалению, комитет по стандартам объявил WebSQL устаревшим и никогда не поддерживал браузеры, отличные от Blink / Webkit.
Есть несколько библиотек, которые предоставляют абстракцию над автономным хранилищем:
- localForage — простой localStorage-подобный API
- IDBWrapper — кросс-браузерная оболочка IndexedDB
- PouchDB — решение для хранения на стороне клиента, вдохновленное CouchDB. Он поддерживает автоматическую синхронизацию с бэкэндом, если используется CouchDB.
Приложение ContactBook
Теперь давайте посмотрим, как добавить автономную поддержку в веб-приложение. Наше примерное приложение — это базовая книга контактов:
У нас есть список контактов слева и форма детализации справа, используемая для редактирования контактов. У контакта есть три поля: имя, фамилия и телефон.
Вы можете найти исходный код приложения на GitHub. Для запуска приложения вам понадобится Node.js. Если вы не уверены в этом шаге, вы можете следовать нашему руководству для начинающих по npm .
Начните с загрузки исходных текстов и запуска следующих команд из папки проекта:
$ npm install
$ npm run serve
Как насчет бэкэнда? Мы используем pouchdb-сервер для предоставления REST API через хранилище CouchDB и http-сервер для обслуживания ресурсов внешнего интерфейса.
Наш раздел scripts
package.json
"scripts": {
"serve": "npm-run-all -p serve-front serve-backend",
"serve-front": "http-server -o",
"serve-backend": "pouchdb-server -d db"
},
Пакет npm-run-all
Запускаем оба сервера: http-server
pouchdb-server
Теперь давайте посмотрим на реализацию автономной поддержки активов приложения.
Автономные активы
Каталог / public содержит все ресурсы для приложения:
- /css/style.css — таблица стилей приложения
- / js / ext — каталог, содержащий внешние библиотеки (PouchDB и Babel для использования синтаксиса ES2015)
- /js/app.js — основной скрипт приложения
- /js/register-service-worker.js — скрипт, который регистрирует сервисного работника
- /js/store.js — класс адаптера для работы с хранилищем PouchDB
- /contactbook.appcache — манифест AppCache
- /index.html — разметка приложения
- /service-worker.js — источник работника сервиса
Путешествие начинается с регистрации сервисного работника. Вот регистрационный код в register-service-worker.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
}).then(function() {
// success
}).catch(function(e) {
// failed
});
}
Сначала мы проверяем, что serviceWorker
Если да, мы вызываем метод register
/service-worker.js
Параметры являются необязательными, а корень /
scope
Важно : чтобы иметь возможность использовать корень приложения в качестве области действия, сценарий работника службы должен быть расположен в корневом каталоге приложения.
Метод register
Promise
Жизненный цикл сервисного работника начинается с установки. Мы можем обработать событие install
var CACHE_NAME = 'contact-book-v1';
var resourcesToCache = [
'/',
'/css/style.css',
'/js/ext/babel.min.js',
'/js/ext/pouchdb.min.js',
'/js/register-service-worker.js',
'/js/store.js',
'/js/app.js'
];
self.addEventListener('install', function(event) {
event.waitUntil(
// open the app browser cache
caches.open(CACHE_NAME)
.then(function(cache) {
// add all app assets to the cache
return cache.addAll(resourcesToCache);
})
);
});
Последнее, что нужно сделать, это обработать событие fetch
self.addEventListener('fetch', function(event) {
event.respondWith(
// try to find corresponding response in the cache
caches.match(event.request)
.then(function(response) {
if (response) {
// cache hit: return cached result
return response;
}
// not found: fetch resource from the server
return fetch(event.request);
})
);
});
Вот и все. Давайте проверим, что это работает:
- запустить приложение с помощью
npm run serve
- открыть URL-адрес http://127.0.0.1:8080/ в Chrome
- остановите веб-сервер с помощью
Ctrl + C
имитации перехода в автономный режим ) - обновить веб-страницу
Приложение все еще доступно. Потрясающие!
AppCache
Проблема с решением, приведенным выше, заключается в том, что работники сервиса имеют ограниченную поддержку браузера . Мы можем реализовать запасное решение, используя широко поддерживаемый AppCache. Читайте больше информации об использовании AppCache здесь .
Основное использование простое и включает в себя два этапа:
-
Определение манифеста кэша приложения
contactbook.appcache
CACHE MANIFEST # v1 2017-30-01 CACHE: index.html css/style.css js/ext/babel.min.js js/ext/pouchdb.min.js js/store.js js/app.js
Для нашего простого приложения мы определяем один раздел
CACHE
-
Ссылка на файл манифеста из HTML:
<html manifest="contactbook.appcache" lang="en">
Вот и все. Давайте откроем страницу в браузере, не поддерживающем Service Workers, и протестируем ее так же, как раньше.
Автономные данные
Возможность кэшировать активы — это прекрасно. Но этого недостаточно. То, что делает приложение живым, это уникальные данные. Мы собираемся использовать PouchDB в качестве хранилища данных на стороне клиента. Это мощный, простой в использовании и обеспечивает синхронизацию данных из коробки.
Если вы не знакомы с ним, ознакомьтесь с этим введением в PouchDB .
Хелпер класс Store
class Store {
constructor(name) {
this.db = new PouchDB(name);
}
getAll() {
// get all items from storage including details
return this.db.allDocs({
include_docs: true
})
.then(db => {
// re-map rows to collection of items
return db.rows.map(row => {
return row.doc;
});
});
}
get(id) {
// find item by id
return this.db.get(id);
}
save(item) {
// add or update an item depending on _id
return item._id ?
this.update(item) :
this.add(item);
}
add(item) {
// add new item
return this.db.post(item);
}
update(item) {
// find item by id
return this.db.get(item._id)
.then(updatingItem => {
// update item
Object.assign(updatingItem, item);
return this.db.put(updatingItem);
});
}
remove(id) {
// find item by id
return this.db.get(id)
.then(item => {
// remove item
return this.db.remove(item);
});
}
}
Код класса Store
Теперь наш основной компонент приложения может использовать Store
class ContactBook {
constructor(storeClass) {
// create store instance
this.store = new storeClass('contacts');
// init component internals
this.init();
// refresh the component
this.refresh();
}
refresh() {
// get all contacts from the store
this.store.getAll().then(contacts => {
// render retrieved contacts
this.renderContactList(contacts);
});
}
...
}
Класс Store
Как только магазин создан, он используется в методе refresh
Инициализация приложения выглядит следующим образом:
new ContactBook(Store);
Другие методы приложения взаимодействуют с магазином:
class ContactBook {
...
showContact(event) {
// get contact id from the clicked element attributes
var contactId = event.currentTarget.getAttribute(CONTACT_ID_ATTR_NAME);
// get contact by id
this.store.get(contactId).then(contact => {
// show contact details
this.setContactDetails(contact);
// turn off editing
this.toggleContactFormEditing(false);
})
}
editContact() {
// get id of selected contact
var contactId = this.getContactId();
// get contact by id
this.store.get(this.getContactId()).then(contact => {
// show contact details
this.setContactDetails(contact);
// turn on editing
this.toggleContactFormEditing(true);
});
}
saveContact() {
// get contact details from edit form
var contact = this.getContactDetails();
// save contact
this.store.save(contact).then(() => {
// clear contact details form
this.setContactDetails({});
// turn off editing
this.toggleContactFormEditing(false);
// refresh contact list
this.refresh();
});
}
removeContact() {
// ask user to confirm deletion
if (!window.confirm(CONTACT_REMOVE_CONFIRM))
return;
// get id of selected contact
var contactId = this.getContactId();
// remove contact by id
this.store.remove(contactId).then(() => {
// clear contact details form
this.setContactDetails({});
// turn off editing
this.toggleContactFormEditing(false);
// refresh contact list
this.refresh();
});
}
Это основные операции с использованием методов CRUD магазина:
-
showContact
-
editContact
-
saveContact
-
removeContact
Теперь, если вы добавите контакты в автономном режиме и обновите страницу, данные не будут потеряны.
Но есть «но»…
Синхронизация данных
Все это прекрасно работает, но все данные хранятся локально в браузере. Если мы откроем приложение в другом браузере, мы не увидим изменений.
Нам нужно реализовать синхронизацию данных с сервером. Реализация двусторонней синхронизации данных не является тривиальной проблемой. К счастью, это обеспечивается PouchDB, если у нас есть CouchDB на бэкэнде.
Давайте немного изменим наш класс Store
class Store {
constructor(name, remote, onChange) {
this.db = new PouchDB(name);
// start sync in pull mode
PouchDB.sync(name, `${remote}/${name}`, {
live: true,
retry: true
}).on('change', info => {
onChange(info);
});
}
Мы добавили два параметра в конструктор:
-
remote
-
onChange
Метод PouchDB.sync
Параметр live
retry
Нам нужно соответствующим образом изменить класс приложения и передать необходимые параметры конструктору Store
class ContactBook {
constructor(storeClass, remote) {
this.store = new storeClass('contacts', remote, () => {
// refresh contact list when data changed
this.refresh();
});
...
}
Конструктор класса главного приложения теперь принимает удаленный URL, который передается в хранилище. onChange
refresh
Инициализация приложения должна быть обновлена:
new ContactBook(Store, 'http://localhost:5984');
Выполнено! Теперь наше приложение позволяет редактировать список контактов в автономном режиме. Когда приложение подключено к сети, данные синхронизируются с внутренним хранилищем.
Давайте проверим это:
- запустить веб-сервер с
$ npm run serve
- откройте URL http://127.0.0.1:8080/ в двух разных браузерах
- остановите веб-сервер, нажав
Ctrl + C
- редактировать список контактов в обоих браузерах
- снова запустите веб-сервер с
$ npm run serve
- проверьте список контактов в обоих браузерах (он должен быть актуальным в соответствии с изменениями в обоих браузерах)
Замечательно, мы сделали это!
Проверьте полный исходный код приложения на GitHub.
Вывод
Обеспечение работы в автономном режиме приобретает все большую ценность сегодня. Возможность использовать приложение с нестабильным соединением в транспорте или находиться в автономном режиме в самолете имеет решающее значение для часто используемых приложений. Это также касается улучшения производительности приложения.
Для поддержки оффлайн нам нужно было позаботиться о:
- кэширование ресурсов приложения — используйте Service Workers с откатом до AppCache, пока первый не будет поддержан всеми современными браузерами
- хранение данных на стороне клиента — используйте автономное хранилище браузера, например IndexedDB, с одной из доступных библиотек
Мы только что посмотрели, как все это можно реализовать. Надеюсь, вам понравилось читать. Пожалуйста, поделитесь своими мыслями о теме в комментариях!
Эта статья была рецензирована Джеймсом Колсом и Крейгом Баклером . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!