Medium service worker

Разбираемся с service worker API

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

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

Чтобы ваш сайт или ваше приложение работали, браузер запрашивает необходимые ресурсы, такие как страницы HTML, JavaScript, изображения и шрифты. В прошлом управление всем этим было прерогативой браузера. Если у браузера не было доступа к сети, вы могли увидеть сообщение о работе в автономном режиме и недоступности ресурсов. Были техники, с помощью которых вы могли улучшить кэширование ресурсов, но последнее слово всегда оставалось за браузером.

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

Несколько лет назад появились новые надежды с появлением Application Cache (или AppCache), ожидалось, что с его помощью можно будет диктовать браузеру, как обрабатывать различные ресурсы, это должно было помочь сайту или приложению работать в офлайне. Но за простым синтаксисом Yet AppCache скрывались отсутствие гибкости и изначально неудачная архитектура.

Сервис-воркеры относительно молоды и делают то, что делает AppCache, а также многое другое. Но на первый взгляд они кажутся непростыми. Спецификация написана абстрактно и тяжело, для работы используются множественные API: cache, fetch и т.д. При этом сервис-воркеры обладают большим функционалом — push-уведомления, а скоро и фоновая синхронизация. В сравнении с Application Cache это все выглядит сложным.

AppCache (который постепенно уходит) был достаточно легким в момент изучения и ужасным для всех последующих моментов, сервис-воркеры требуют больших усилий для первоначального освоения, но в то же время они мощнее и полезнее, а в случае каких-либо ошибок в коде не нанесут особого вреда.

Базовая концепция сервис-воркера

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

Сервис-воркер выполняет скрипты в отдельном потоке браузера относительно страницы, которую он контролирует. Существуют способы сообщения между воркерами и страницами, но сами воркеры выполняются в изолированном пространстве имен. Это, например, означает, что у вас нет доступа к DOM этих страниц. Я представляю сервис-воркера как разновидность изолированной от страницы вкладки, это не совсем точная, но полезная метафора для начала понимания.

JavaScript в сервис-воркере должен быть неблокирующим. Для этого надо использовать асинхронный API. Например, вы не можете использовать localStorage в сервис-воркере (потому что это синхронный API). Смешно, но даже зная это, я пошла на риск нарушения этого правила, вы это еще увидите.

Регистрация сервис-воркера

Чтобы сервис-воркер заработал, его надо зарегистрировать. Регистрация делается вне сервис-воркера, другой страницей или скриптом вашего сайта. На моем сайте есть глобальный скрипт main.js, который подключен на каждой странице, именно в этом скрипте регистрируется сервис-воркер.

Когда вы регистрируете сервис-воркер, вы опционально можете указать ему область действия. Вы можете дать сервис-воркеру инструкцию обрабатывать только часть сайта (например, /blog/) или весь сайт (/).

События и жизненный цикл

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

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

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

После окончания стадии install происходит активация сервис-воркера. Это значит, что сервис-воркер уже контролирует свою зону деятельности и может выполнять свои задачи. Событие activate не слишком интригующе для нового сервис-воркера, но мы увидим, как оно полезно при обновлении версии сервис-воркера.

Время события активации у нового сервис-воркера и обновляемой версии существующего сервис-воркера различается. Если в браузере не зарегистрирована предыдущая версия данного сервис-воркера, активация происходит немедленно после окончания установки.

Как только установка и активация завершены, они больше не будут вызываться до момента скачивания и регистрации новой версии сервис-воркера.

Помимо установки и активации нас в первую очередь интересует событие fetch, именно оно позволит сделать наш сервис-воркер полезным. Кроме него, есть еще несколько полезных событий, например, события синхронизации и уведомлений.

Если вы заинтересовались этой темой, вам стоит почитать об интерфейсах, реализуемых сервис-воркерами. Именно за счет имплементации интерфейсов сервис-воркеры получают основную массу своих событий и большую часть своей функциональности.

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

API сервис-воркера

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

Требования сервис-воркеров

Также важно отметить, что для работы сервис-воркеров необходим HTTPS. С одним важным и полезным исключением: чтобы лишний раз не издеваться над разработчиками, на localhost сервис-воркеры работают с простым http.

Регистрация, установка и активация сервис-воркера

Для установки и активации сервис-воркера мы будем прослушивать события install и activate и воздействовать на них.

Мы можем начать с пустого файла с нашим сервис-воркером и добавить в него пару обработчиков событий. В service-worker.js:

self.addEventListener('install', event => {
  // Do install stuff
});

self.addEventListener('activate', event => {
  // Do activate stuff: This will come later on.
});

Теперь нам надо сказать страницам сайта, чтобы они использовали сервис-воркер.

Запомните, регистрация происходит вне сервис-воркера — в моем случае на каждой странице сайта для этого подключается скрипт /js/main.js.

Вот его содержимое:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js', {
    scope: '/'
  });
}

Предварительное кэширование статических ресурсов при установке

Я хочу использовать стадию установки для предварительного кэширования отдельных ресурсов моего сайта.

  • предварительно кэшируя некоторые статические ресурсы (изображения, CSS, JavaScript), которые используются на многих страницах сайта, я могу ускорить время загрузки, вытягивая их из кэша, вместо того, чтобы качать их из сети при последующих загрузках страницы.
  • предварительно кэшируя автономную запасную страницу, я могу показать красивую страницу в случаях, когда я не могу выполнить запрос страницы, когда пользователь в офлайне.

Для этого необходимы следующие шаги:

  • Прикажите событию install не завершаться, пока вы не выполните то, что вы хотите с помощью event.waitUntil.
  • Откройте соответствующий кэш и закачайте в него статические ресурсы, используя Cache.addAll. В терминологии прогрессивных веб-приложенийэти ресурсы будут “оболочкой приложения”.

Расширим обработчик install в /serviceWorker.js:

const CACHE_URLS = [
  '/dist/offline.html',
  '/dist/vendors.js',
  '/dist/common.js',
  '/dist/common.css'
];

function precache() {
  return openCache()
    .then((cache) => {
      return cache.addAll(CACHE_URLS);
    })
    .then(() => self.skipWaiting());
}

self.addEventListener('install', event => {
    event.waitUntil(precache());
});

Сервис-воркер реализует интерфейс CacheStorage, делающий свойство caches доступным глобально в нашем сервис-воркере. В caches есть несколько полезных методов, например, open и delete.

Вы можете видеть здесь работу промисов: caches.open возвращает Promise, занимающийся объектом cache после успешного отрытия статического кэша (static), addAll также возвращает промис, контролирующий сохранение всех переданных ресурсов в кэше.

Я говорю event подождать, пока Promise, возвращаемый моей функцией-обработчиком разрешится успешно. Теперь мы можем быть уверены, что все предварительно кэшируемые элементы рассортируются до завершения установки.

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

Выборка с помощью сервис-воркеров

До сих пор у нашего сервис-воркера был обработчик install и ничего больше. Магия нашего сервис-воркера начинается, когда запускаются события fetch.

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

Всякий раз, когда браузер хочет загрузить ресурс, находящийся в зоне действия сервис-воркера, мы можем узнать об этом добавив обработчик eventListener в service-worker.js:

self.addEventListener('fetch', event => {
  // … Perhaps respond to this fetch in a useful way?
});

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

В итоге мы оказываемся со следующей базовой логикой в файле service-worker.js:

function addToCache(request, response) {
  return openCache()
    .then((cache) => {
      return cache.put(request, response.clone());
    });
}

function fromInternet(request) {
  return fetch(request)
    .then((response) => {
      if (response) {
        return addToCache(request, response)
          .then(() => response);
      }

      return response;
    });
}

function fromCache(request) {
  return openCache()
    .then((cache) => {
      return cache
        .match(request)
        .then((matching) => {
          if (matching) {
            return matching;
          }

          return fromInternet(request);
        });
    });
}

self.addEventListener('fetch', (event) => {
  const request = event.request;
  const url = new URL(request.url);
  const acceptHeader = request.headers.get('Accept');

  if (
    (request.method === 'GET') &&
    (acceptHeader.indexOf('text/html') === -1) &&
    (
      (url.origin === self.location.origin) ||
      (url.origin.indexOf('fonts.gstatic.com') !== -1) ||
      (url.origin.indexOf('fonts.googleapis.com') !== -1)
    )
  ) {
    event.respondWith(fromCache(request));
  } else if (
    (event.request.method === 'GET') &&
    (acceptHeader.indexOf('text/html') !== -1)
  ) {
    event.respondWith(
      fetch(request)
        .catch(() => {
          return caches.match(OFFLINE_URL);
        })
    );
  }
});

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

Если мы все сделали, но у нас нет ничего в кэше, нам желательно подготовить запасной вариант. Для HTML это может быть страница `/offline/`. Это страница сообщает пользователю, что он в офлайне и его запрос не может быть выполнен сейчас, своего рода аналог страницы 404.

Критерии валидных запросов

Итак, давайте продолжим определять, применим ли текущий запрос на загрузку ресурса для нашего сервис-воркера. Для моего сайта критерии следующие:

  • Запрошенный URL должен представлять нечто, что я хочу закэшировать или ответить иным образом. Путь к нему должен соответствовать регулярному выражению валидного пути.
  • Метод HTTP-запроса должен быть GET.
  • Запрашиваемый ресурс должен находиться на моем домене или CDN google (для кеширования шрифтов).

Конечно, критерии здесь именно для моего сайта и на других сайтах они будут отличаться. event.request это объект Request в котором содержатся все виды данных, на которые вы можете взглянуть, чтобы определиться с желаемым поведением обработчика.

Ответы могут использоваться только один раз.

С полученным ответом нам надо сделать две вещи:

  • закэшировать его
  • ответить на событие с ним (т.е. вернуть его)

Так как объекты Response могут использоваться лишь один раз, клонирование позволяет создать копию для нужд кэша:

const copy = response.clone();

Стратегия Cache-First

Логика загрузки остальных ресурсов (не HTML) использует стратегию Cache-First (т.е. сначала извлекаются ресурсы из кэша). Изображения и прочее статическое содержимое редко меняется на сайте, таким образом, запросив в начале кэш мы избегаем лишнего сетевого обмена.

Этот подход включает следующие шаги:

  • пытаемся извлечь ресурс из кэша
  • в случае неудачи, извлекаем его из сети (кэшируя на ходу)
  • в случае очередного фэйла используем офлайновый запасной вариант

Первая версия сервис-воркера готова

Первая версия нашего сервис-воркера готова. У нас есть обработчик install и открытый обработчик fetch, способный отвечать на запросы о загрузке, а также предоставлять кэшированные ресурсы и страницу-заглушку для автономного режима.

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

Версионирование и обновление сервис-воркера

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

Я хочу подчеркнуть, что существуют автоматические инструменты, чтобы сделать сервис-воркера частью вашего рабочего процесса, типа разработки Google Service Worker Precache. Нет необходимости заниматься версионированием вручную. Однако мой сайт достаточно прост для ручного версионирования изменений сервис-воркера. Он состоит из:

  • простой строки для индикации версии
  • имплементации в обработчике activate очистки старых версий
  • обновлении обработчика install, чтобы обновленные сервис-воркеры активировались быстрее

Добавляем обработчик активации

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

Очищаем старый кэш

Для очистки старого кэша мы можем использовать следующий код:

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches
      .keys()
      .then((cacheKeys) => {
        const deletePromises = cacheKeys
          .filter(key => key.indexOf(CACHE_NAME) !== 0)
          .map(oldKey => caches.delete(oldKey));

        return Promise.all(deletePromises);
      })
      .then(() => self.clients.claim())
  );
});

Вот и все! Воркер готов к работе.

Итоговый код можно найти здесь.