- PVSM.RU - https://www.pvsm.ru -

Web-приложения в режиме offline. ServiceWorker и CacheStorage

Web-приложения в режиме offline. ServiceWorker и CacheStorage - 1

О чём речь?

Всё чаще возникает задача научить frontend-приложение работать в автономном режиме. Это значит придать web-приложению свойство mobile- или desktop-программы — функционировать в отсутствии связи с Интернет, а также в случае отказа сервера.

Цель — оградить пользователя от проблем соединения на его устройстве. Как было бы обидно не сохранить созданные в google docs таблицы из-за потери wi-fi в ближайшем фастфуде!

Для этого нужно инициализировать приложение в браузере и затем закэшировать ресурсы, минимально необходимые для функционирования. После этого приложение возможно запустить из кэша в случае недоступности сервера.

Решение задачи заключается в следующем:

  1. при первом посещении web-страницы, получить с сервера “статические” ресурсы в виде html-, css-, js-файлов, спрайтов и пр.;
  2. закэшировать ресурсы на стороне клиента средствами браузера;
  3. в дальнейшем при запросе этих же файлов выдавать их из кэша в том случае, если отсутствует соединение с сервером;
  4. обновлять изменённые ресурсы в кэше.

Об этом уже есть замечательная статья [1], но с тех пор кое-что изменилось. Ранее была популярна технология ApplicationCache, о которой можно почитать здесь [2]. С ApplicationCache был ряд проблем, такой технологии не хватало гибкости и на данный момент она устарела и не рекомендуется к использованию.

Теперь на арену выходит CacheStorage, с которым можно работать в ServiceWorker’е.

ServiceWorker

ServiceWorker [3] — это новая технология, позволяющая запускать javascript-код в браузере в фоновом режиме — аналог сервисов (служб) в операционных системах. ServiceWorker запускается с web-ресурса и продолжает работать в браузере независимо от приложения, которое его инициализировало.

Часто цель применения ServiceWorker это получение push-уведомлений в браузере и контроль кэшируемых ресурсов, последнее как раз наш случай.

CacheStorage

CacheStorage [4] представляет собой контейнер для хранения кэша сетевых ресурсов. Глобальный объект CacheStorage доступен по имени caches. Его составляющие это объекты типа Cache [5].

Cache — это именованное хранилище из пар: объект Request — объект Response. Для каждого закэшированного ресурса экземпляр Cache будет хранить request и response, созданные функцией fetch [6].

Как это выглядит на практике?

Теперь разберём всё это на небольшом тестовом приложении. Допустим, что файлы расположены на локальном сервере по адресу localhost/test_serviceworker [7]. Поставим задачи. Для того, чтобы управлять кэшированием, необходимо:

  1. создать serviceWorker, который будет работать в браузере, независимо от наличия/отсутствия доступа к сети;
  2. сложить в caches ресурсы, которые должны быть закэшированы;
  3. в коде serviceWorker’а повесить обработчик на событие fetch — событие, возникающее при запросе сетевого ресурса;
  4. построить в обработчике логику выдачи и обновления кэша.

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

workerLoader.js

// при регистрации указываем на js-файл с кодом serviceWorker’а
// получаем Promise объект
navigator.serviceWorker.register(
   '/test_serviceworker/appCache.js'
).then(function(registration) {
    // при удачной регистрации имеем объект типа ServiceWorkerRegistration  
    console.log('ServiceWorker registration', registration);
    // строкой ниже можно прекратить работу serviceWorker’а
    //registration.unregister();
}).catch(function(err) {
    throw new Error('ServiceWorker error: ' + err);
});

Следующим шагом пишем простой код ServiceWorker’а.

appCache.js

self.addEventListener('install', function(event) {
    // инсталляция
    console.log('install', event);
});

self.addEventListener('activate', function(event) {
    // активация
    console.log('activate', event);
});

Важно иметь в виду, что serviceWorker никак не связан с глобальной областью видимости приложения, в котором был зарегистрирован, и вообще не имеет объекта window. В self у него находится объект типа ServiceWorkerGlobalScope, на котором устанавливаются обработчики событий.

После предпринятых действий и обновления страницы в chrome по адресу chrome://inspect/#service-workers можно увидеть примерно такую картину:

Web-приложения в режиме offline. ServiceWorker и CacheStorage - 2

На данном скриншоте в браузере запущено в фоновом режиме три js-файла, первый из которых является кэширующим сервисом из данного примера.

Далее в коде serviceWorker’а на этапе инсталляции создаём первоначальный кэш из необходимых ресурсов.

appCache.js

// наименование для нашего хранилища кэша
var CACHE_NAME = 'app_serviceworker_v_1',
// ссылки на кэшируемые файлы
    cacheUrls = [
        '/test_serviceworker/',
        '/test_serviceworker/index.html',
        '/test_serviceworker/css/custom.css',
        '/test_serviceworker/images/icon.png',
        '/test_serviceworker/js/main.js'
];

self.addEventListener('install', function(event) {
    // задержим обработку события
    // если произойдёт ошибка, serviceWorker не установится
    event.waitUntil(
        // находим в глобальном хранилище Cache-объект с нашим именем
        // если такого не существует, то он будет создан
        caches.open(CACHE_NAME).then(function(cache) {
            // загружаем в наш cache необходимые файлы
            return cache.addAll(cacheUrls);
        })
    );
});

Обратите внимание, все ссылки указаны относительно корня. Также среди кэшируемых ресурсов нет файла workerLoader.js, который регистрирует serviceWorker. Его кэшировать не желательно, т.к. в режиме offline приложение и без него будет работать. Но если срочно будет необходимо отключить serviceWorker, могут возникнуть проблемы. Пользователи вынуждены будут ждать пока serviceWorker обновится самостоятельно (посредством сравнения содержимого).

Далее добавляем в код serviceWorker’а обработчик события fetch. И на запрос ресурса выдаём его из кэша.

appCache.js

self.addEventListener('fetch', function(event) {
    event.respondWith(
        // ищем запрашиваемый ресурс в хранилище кэша
        caches.match(event.request).then(function(cachedResponse) {

            // выдаём кэш, если он есть
            if (cachedResponse) {
                return cachedResponse;
            }

            // иначе запрашиваем из сети как обычно
            return fetch(event.request);
        })
    );
});

Но не всегда всё так просто. Допустим наши файлы статики поменялись на сервере. Усложним наш код. Проверим дату последнего обновления ресурса вытащив параметр last-modified из HTTP заголовков. И при необходимости подгрузим свежую версию файла и обновим кэш.

appCache.js

// период обновления кэша - одни сутки
var MAX_AGE = 86400000;

self.addEventListener('fetch', function(event) {

    event.respondWith(
        // ищем запрошенный ресурс среди закэшированных
        caches.match(event.request).then(function(cachedResponse) {
            var lastModified, fetchRequest;

            // если ресурс есть в кэше
            if (cachedResponse) {
                // получаем дату последнего обновления
                lastModified = new Date(cachedResponse.headers.get('last-modified'));
                // и если мы считаем ресурс устаревшим
                if (lastModified && (Date.now() - lastModified.getTime()) > MAX_AGE) {
                    
                    fetchRequest = event.request.clone();
                    // создаём новый запрос
                    return fetch(fetchRequest).then(function(response) {
                        // при неудаче всегда можно выдать ресурс из кэша
                        if (!response || response.status !== 200) {
                            return cachedResponse;
                        }
                        // обновляем кэш
                        caches.open(CACHE_NAME).then(function(cache) {
                            cache.put(event.request, response.clone());
                        });
                        // возвращаем свежий ресурс
                        return response;
                    }).catch(function() {
                        return cachedResponse;
                    });
                }
                return cachedResponse;
            }

            // запрашиваем из сети как обычно
            return fetch(event.request);
        })
    );
});

Примечание, для повторного запроса используются клоны request и response. Только один раз возможно отправить request и только один раз можно прочитать response. Из-за этого ограничения мы создаём копии этих объектов.

Подобным образом можно контролировать процесс кэширования. Теперь заметно явное преимущество перед appcache manifest в его декларативном стиле. Для разработчика открыта возможность оперировать HTTP заголовками, статус-кодами, содержимым и реализовать любую логику для отдельно взятых ресурсов.

Выводы

  • Существенный плюс — гибкая система кэширования. Что и когда кэшировать — всё в наших руках (попрощайтесь с ApplicationCache!).
  • Не стоит использовать такую методику для “нестатических” ресурсов, т.е. для данных получаемых от сервера. Лучше это делать не в сервисе, а внутри front-приложения на уровне модели и использовать при этом, например, localStorage.
  • Существует такой нюанс: serviceWorker разрешено загрузить только по HTTPS либо с локального сервера. Неприятное ограничение, но в то же время это правильно. HTTPS становится стандартом для популярных сервисов.
  • Самый главный недостаток. ServiceWorker и CacheStorage — обе технологии экспериментальные на текущее время. И поддержка есть у современных Mozilla, Opera и браузеров от Google. Однако, и это не мало [8].

Тема web-приложений в режиме offline существует давно. Возможно ServiceWorker и CacheStorage лишь временное решение. Тем не менее, это уже имеет смысл использовать. Даже если данные технологии устареют, производители браузеров создадут что-нибудь в качестве замены. Стоит только следить за прогрессом в этом направлении!

Лучшими материалами для написания этой статьи были:

http://www.html5rocks.com/en/tutorials/service-worker/introduction/ [9],
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers [3]. Несмотря на их наличие, решил поделиться информацией на русском языке.

Автор: dolgo

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/115016

Ссылки в тексте:

[1] статья: https://habrahabr.ru/post/117123/

[2] здесь: https://habrahabr.ru/post/151815/

[3] ServiceWorker: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

[4] CacheStorage: https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage

[5] Cache: https://developer.mozilla.org/en-US/docs/Web/API/Cache

[6] fetch: https://learn.javascript.ru/fetch

[7] localhost/test_serviceworker: http://localhost/test_serviceworker/

[8] и это не мало: http://caniuse.com/#feat=serviceworkers

[9] http://www.html5rocks.com/en/tutorials/service-worker/introduction/: http://www.html5rocks.com/en/tutorials/service-worker/introduction/

[10] Источник: https://habrahabr.ru/post/279291/