Статьи

Создание автономных веб-приложений с использованием сервисных работников и PouchDB

Два разработчика, использующие автономные веб-приложения в поезде

Оффлайн-приложения становятся все более популярными. Автономная поддержка настолько важна, что в настоящее время принято говорить о подходе «Offline First», когда он становится основным фактором. Он также набирает популярность с ростом Progressive Web Apps.

В этом посте мы рассмотрим, как добавить автономную поддержку в основное веб-приложение со списком контактов путем реализации кэширования ресурсов, хранения данных на стороне клиента и синхронизации с удаленным хранилищем данных.

Исходный код приложения доступен на GitHub.

Зачем нужна поддержка в автономном режиме?

Почему мы должны заботиться о поддержке в автономном режиме?

Я сам провожу больше часа в поезде каждый день. Я не хочу тратить это время впустую, поэтому по дороге беру свой ноутбук с собой. Я использую сотовую сеть, чтобы быть онлайн. Соединение не надежное, поэтому время от времени я его теряю. Мой пользовательский опыт зависит от веб-приложения, которое я использую. Лишь несколько приложений с хорошей автономной поддержкой ведут себя так, как ожидается, и потеря соединения прозрачна. Некоторые ведут себя странно, поэтому, когда я обновляю страницу, я теряю данные. Большинство вообще не поддерживают автономный режим, и мне приходится ждать стабильного соединения, чтобы использовать их.

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

Еще одним важным преимуществом автономной поддержки является повышение производительности. Действительно, браузеру не нужно ждать загрузки ресурсов с сервера. То же самое для данных, когда-то хранящихся на клиенте.

Таким образом, нам нужно в автономном режиме:

  1. иметь возможность использовать приложения даже при нестабильном соединении (сотовая сеть в поезде)
  2. иметь возможность работать без подключения к сети (на самолете)
  3. повысить производительность

Прогрессивные веб-приложения

Концепция Google Progressive Web Apps (PWA) — это методология, предназначенная для предоставления веб-приложений, которые предоставляют UX собственных мобильных приложений. PWA включает поддержку в автономном режиме, но она также охватывает гораздо больше:

  • Отзывчивость — поддержка разных форм-факторов: мобильный, планшетный, настольный
  • Web App Manifest — для установки приложения на домашний экран
  • App Shell — шаблон проектирования, в котором базовая оболочка приложения пользовательского интерфейса отделена от содержимого, загруженного впоследствии
  • Push-уведомления — чтобы получать «мгновенные» обновления с сервера

Адди Османи написала отличное вступительное сообщение о PWA .

В этой статье мы сосредоточимся только на одном аспекте: автономной поддержке.

Определение автономной поддержки

Давайте уточним, что требуется для поддержки в автономном режиме. Нам нужно позаботиться о двух аспектах:

  1. ресурсы приложения — кеширование HTML, JS-скрипты, таблицы стилей CSS, изображения
  2. данные приложения — хранение данных на стороне клиента

Активы приложения

Первым решением в 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-сервер для обслуживания ресурсов внешнего интерфейса.

Наш раздел scriptspackage.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-serverpouchdb-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

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

Метод registerPromise

Жизненный цикл сервисного работника начинается с установки. Мы можем обработать событие 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);
      })
  );
});

Вот и все. Давайте проверим, что это работает:

  1. запустить приложение с помощью npm run serve
  2. открыть URL-адрес http://127.0.0.1:8080/ в Chrome
  3. остановите веб-сервер с помощью Ctrl + Cимитации перехода в автономный режим )
  4. обновить веб-страницу

ресурсы приложения в автономном режиме

Приложение все еще доступно. Потрясающие!

AppCache

Проблема с решением, приведенным выше, заключается в том, что работники сервиса имеют ограниченную поддержку браузера . Мы можем реализовать запасное решение, используя широко поддерживаемый AppCache. Читайте больше информации об использовании AppCache здесь .

Основное использование простое и включает в себя два этапа:

  1. Определение манифеста кэша приложения 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

  2. Ссылка на файл манифеста из HTML:

     <html manifest="contactbook.appcache" lang="en">
    

Вот и все. Давайте откроем страницу в браузере, не поддерживающем Service Workers, и протестируем ее так же, как раньше.

ресурсы приложения в автономном режиме с appcache

Автономные данные

Возможность кэшировать активы — это прекрасно. Но этого недостаточно. То, что делает приложение живым, это уникальные данные. Мы собираемся использовать 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 Параметр liveretry

Нам нужно соответствующим образом изменить класс приложения и передать необходимые параметры конструктору Store

 class ContactBook {

  constructor(storeClass, remote) {
    this.store = new storeClass('contacts', remote, () => {
      // refresh contact list when data changed
      this.refresh();
    });

    ...
  }

Конструктор класса главного приложения теперь принимает удаленный URL, который передается в хранилище. onChangerefresh

Инициализация приложения должна быть обновлена:

 new ContactBook(Store, 'http://localhost:5984');

Выполнено! Теперь наше приложение позволяет редактировать список контактов в автономном режиме. Когда приложение подключено к сети, данные синхронизируются с внутренним хранилищем.

Давайте проверим это:

  1. запустить веб-сервер с $ npm run serve
  2. откройте URL http://127.0.0.1:8080/ в двух разных браузерах
  3. остановите веб-сервер, нажав Ctrl + C
  4. редактировать список контактов в обоих браузерах
  5. снова запустите веб-сервер с $ npm run serve
  6. проверьте список контактов в обоих браузерах (он должен быть актуальным в соответствии с изменениями в обоих браузерах)

офлайн-contactbook-демо

Замечательно, мы сделали это!

Проверьте полный исходный код приложения на GitHub.

Вывод

Обеспечение работы в автономном режиме приобретает все большую ценность сегодня. Возможность использовать приложение с нестабильным соединением в транспорте или находиться в автономном режиме в самолете имеет решающее значение для часто используемых приложений. Это также касается улучшения производительности приложения.

Для поддержки оффлайн нам нужно было позаботиться о:

  • кэширование ресурсов приложения — используйте Service Workers с откатом до AppCache, пока первый не будет поддержан всеми современными браузерами
  • хранение данных на стороне клиента — используйте автономное хранилище браузера, например IndexedDB, с одной из доступных библиотек

Мы только что посмотрели, как все это можно реализовать. Надеюсь, вам понравилось читать. Пожалуйста, поделитесь своими мыслями о теме в комментариях!

Эта статья была рецензирована Джеймсом Колсом и Крейгом Баклером . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!