Введение в Service Worker'ы
20.12.2016

Введение в Service Worker'ы

Offline-режим, периодическая фоновая синхронизация, push-уведомления — этот функционал нативных приложений уверенно приходит в web. Service Worker’ы предоставляют для этого техническую возможность.

Что такое Service Worker

Service Worker — это скрипт, который браузер запускает в фоновом режиме, отдельно от страницы, открывая дверь для возможностей, не требующих веб-страницы или взаимодействия с пользователем. Сегодня они выполняют такие функции как push-уведомления и фоновая синхронизация, в будущем Service Worker’ы будут поддерживать и другие вещи. Ключевая их особенность — это возможность перехватывать и обрабатывать сетевые запросы, включая программное управление кешированием ответов.

До появления Service Worker’ов реализовать работу в offline-режиме позволял другой API — AppCache. Проблема AppCache заключается не только в нескольких «подводных камнях», но и в том, что он отлично работает в single-page приложениях, но не очень хорошо подходит для многостраничных сайтов. Service Worker’ы разрабатывались, чтобы избежать этих проблем.

Что нужно знать о Service Worker:

  • Это JavaScript Worker, поэтому он не может получить доступ к DOM напрямую. Вместо этого, Service Worker может обмениваться данными со страницами, которые он контролирует, реагируя на сообщения, отправленные через интерфейс postMessage, и эти страницы могут манипулировать DOM, если это необходимо.
  • Service Worker представляет собой программируемый сетевой прокси, что позволяет контролировать обработку сетевых запросов со страницы.
  • Он прерывается, когда не используется, и перезапускается заново, если необходим. Поэтому нельзя полагаться на какое-то глобальное состояние в обработчиках onfetch и onmessage. Если необходимо сохранить какую-либо информацию между перезапусками, Service Worker’ы имеют доступ к API IndexedDB.
  • Service Worker’ы широко используют промисы, поэтому стоит хорошо с ними разобраться.
  • Используя Service Worker’ы можно перехватывать запросы, фабриковать или фильтровать ответы. Это мощная, но опасная возможность. Поэтому зарегистрировать Service Worker можно только на странице поверх HTTPS. В процессе разработки вы можете использовать Service Worker на localhost, но чтобы развернуть его на сайте, необходимо будет настроить на сервере HTTPS.

Жизненный цикл Service Worker’а

Service Worker имеет жизненный цикл, полностью отделенный от веб-страницы.

Чтобы установить Service Worker на сайте, необходимо зарегистрировать его на странице. Регистрация Service Worker’а вызовет шаг установки в браузере в фоновом режиме.

Обычно во время установки кешируются некоторые статические ресурсы. Если все файлы успешно закешировались, Service Worker считается установленным. Если не удается скачать или закешировать какой-то из файлов, шаг установки считается неуспешным и Service Worker не будет активирован (т.е. не будет установлен). Если это происходит, не волнуйтесь, установку можно повторить. Зато если Service Worker установился, это гарантирует, что в кеше есть нужные ресурсы.

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

После активации Service Worker будет контролировать все страницы, находящиеся в его области видимости. Страница, на которой Service Worker был зарегистрирован, не будет контролироваться, пока не будет перезагружена. Когда Service Worker контролирует страницу, он находится в одном из двух состояний: он либо прерван, чтобы сэкономить память, либо обрабатывает сетевые запросы и сообщения со страницы.

Ниже показана очень упрощенная версия жизненного цикла Service Worker’а при его первой установке.

Поддержка браузерами

Service Worker’ы на момент написания статьи поддерживаются Firefox и Chrome, в Microsoft Edge они находятся на стадии разработки, а разработчики Safari запланировали реализацию поддержки в пятилетний план развития. Следить за поддержкой Service Workers можно на сайте Джейка Арчибальда.

Регистрация Service Worker

Чтобы установить Service Worker, вам необходимо запустить процесс регистрации на странице. Код ниже сообщает браузеру, в каком JavaScript-файле находится Service Worker.


if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
        navigator.serviceWorker.register('/sw.js').then(function(registration) {
            // Регистрация успешна
            console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function(err) {
            // Регистрация не успешна
            console.log('ServiceWorker registration failed: ', err);
        });
    });
}

В примере выше проверяется, поддерживается ли браузером Service Worker API, и если да, когда страница будет загружена, будет установлен Service Worker из файла /sw.js.

Вы можете спокойно вызывать register() каждый раз при загрузке страницы, браузер определит, был ли Service Worker установлен ранее и обработает это соответствующим образом.

Одна тонкость метода register() заключается в расположении файла Service Worker’а. В примере выше файл находится в корне домена, это означает, что областью видимости этого Service Worker’а будет весь сайт. Другими словами, Service Worker будет получать события fetch со всего домена. Если зарегистрировать Service Worker из файла /example/sw.js, он сможет обрабатывать события только для страниц, URL которых начинается на /example/ (например, /example/page1/, /example/page2/).

Убедиться, что Service Worker зарегистрирован, можно на странице chrome://inspect/#service-workers.

Тестировать работу Service Worker’а удобно в режиме инкогнито, когда вы можете открывать и закрывать окно, при этом старый Service Worker не будет влиять на новый. Любой кеш очищается при закрытии окна в режиме инкогнито.

Установка Service Worker’а

Когда страница запускает процесс регистрации Service Worker’а, тот, в свою очередь, получает событие install. В большинстве случаев в обработчике этого события кешируются необходимые файлы.


self.addEventListener('install', function(event) {
    // установка
});

В обработчике install необходимо сделать следующее:

  • Открыть кеш.
  • Закешировать файлы.
  • Подтвердить, что все необходимые ресурсы были закешированы.

var CACHE_NAME = 'my-site-cache-v1';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/script/main.js'
];

self.addEventListener('install', function(event) {
  // установка
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

В коде выше мы сначала вызываем caсhes.open() с именем нашего кеша, затем добавляем файлы в кеш с помощью caches.addAll(). Эти методы (caches.open() и caches.addAll()) являются цепочкой промисов. Метод event.waitUntil() принимает промис и использует его, чтобы определить, успешно ли прошла установка.

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

Это только один пример, вы можете реализовать другую логику в обработчике или не обрабатывать событие install совсем.

Кеш и обработка запросов

После того, как Service Worker был установлен, а пользователь перешел на другую страницу или обновил текущую, Service Worker может обрабатывать события fetch, как в примере ниже.


self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // ресурс есть в кеше
        if (response) {
          return response;
        }
        return fetch(event.request);
      }
    )
  );
});

Здесь мы обрабатываем событие fetch и в метод event.respondWith() передаем промис, возвращаемый caches.match(). Метод caches.match() ищет совпадение во всех кешах, созданных Service Worker’ом. Если нужный ответ есть в кеше, возвращаем его, если нет, отправляем сетевой запрос. Этот простой пример использует ресурсы, закешированные на этапе установки. Пример ниже кеширует результаты запросов:


self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // ресурс есть в кеше
        if (response) {
          return response;
        }

        /* Важно: клонируем запрос. Запрос - это поток, может быть обработан только раз. Если мы хотим использовать объект request несколько раз, его нужно клонировать */
        var fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(
          function(response) {
            // проверяем, что получен корректный ответ
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            /* ВАЖНО: Клонируем ответ. Объект response также является потоком. */
            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});

В коде выше мы:

  • Добавляем обработчик then для метода fetch().
  • Получив ответ, убеждаемся, что он валидный, статус ответа 200, а тип ответа равен basic. Тип basic означает, что ответ получен с нашего домена, ресурсы с других доменов не будут кешироваться.
  • Если проверки пройдены, клонируем ответ, сохраняем копию в кеш, а оригинальный ответ возвращаем браузеру.

Обновление Service Worker’а

Рано или поздно Service Worker приходится обновить. Для этого необходимо:

  • Обновить файл Service Worker’а. Когда пользователь заходит на сайт, браузер пытается повторно скачать файл Service Worker’а. Если новый файл отличается от текущего, Service Worker считается новым.
  • Если Service Worker новый, срабатывает событие install.
  • На этом этапе старый Service Worker по прежнему контролирует страницу, тогда как новый переходит в состояние waiting. Когда текущие открытые страницы сайта будут закрыты, старый Service Worker будет убит, а новый получит контроль.
  • Когда новый Service Worker получит контроль над страницей, сработает событие activate.

В обработчике activate целесообразно реализовывать удаление ненужных кешей. Почему именно здесь? Если вы удалите какой-либо из старых кешей в обработчике install, старый Service Worker, который в настоящий момент контролирует страницу, не сможет получить к нему доступ.

Допустим, у нас есть кеш ‘my-site-cache-v1’, а мы хотим разделить его на два — отдельно для страниц, отдельно для постов блога. Нам нужно создать два новых кеша: ’pages-cache-v1′ and ’blog-posts-cache-v1′, а кеш ‘my-site-cache-v1’ удалить.

В примере ниже удаляются все кеши, кроме перечисленных в белом списке:


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

  var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1'];

  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Заключение

Service Worker’ы — ключевая технология разработки целого класс веб-приложений — прогрессивных веб-приложений (Progressive Web Applications).

Функционал Service Worker’ов является еще совсем новым, содержит ряд проблем и поддерживается не всеми браузерами. Однако это мощный инструмент, позволяющий разработчику реализовывать в вебе возможности, характерные для нативных приложений.