[Из песочницы] Web-приложения в режиме offline. ServiceWorker и CacheStorage

e8c4df605ccd442e91ef667cf17cb64d.png

О чём речь?


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

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

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

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

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


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

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

ServiceWorker


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

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

CacheStorage


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

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

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


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

  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 можно увидеть примерно такую картину:

00f74889bb6d43d0ab7c408f72511e5d.png

На данном скриншоте в браузере запущено в фоновом режиме три 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. Однако, и это не мало.


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

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

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

© Habrahabr.ru