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

Они выполняются в потоке, отдельном от основного кода JavaScript нашей страницы, и не имеют никакого доступа к структуре DOM. Это представляет подход, отличный от традиционного веб-программирования — API не блокирует и может отправлять и получать сообщения между различными контекстами. Вы можете дать Service Worker что-то для работы и получить результат, когда он будет готов, используя подход, основанный на Promise.

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

Безопасность

Поскольку они настолько мощны, сервис-воркеры могут выполняться только в защищенных контекстах (имеется в виду HTTPS). Если вы хотите сначала поэкспериментировать, прежде чем запускать свой код в производство, вы всегда можете протестировать на локальном хосте или настроить страницы GitHub — оба поддерживают HTTPS.

Сначала офлайн

Паттерн «сначала оффлайн» или «сначала кешировать» — самая популярная стратегия предоставления контента пользователю. Если ресурс кэширован и доступен в автономном режиме, сначала верните его, прежде чем пытаться загрузить с сервера. Если его еще нет в кэше, загрузите его и кэшируйте для будущего использования.

«Прогресс» в PWA

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

Сервисные работники в приложении js13kPWA

Достаточно теории — давайте посмотрим исходный код!

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

Мы начнем с просмотра кода, который регистрирует новый Service Worker в файле app.js:

ПРИМЕЧАНИЕ. Мы используем синтаксис стрелочных функций es6 в реализации Service Worker.

if('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./pwa-examples/js13kpwa/sw.js');
};

Если API сервис-воркера поддерживается в браузере, он регистрируется на сайте с помощью метода ServiceWorkerContainer.register(). Его содержимое находится в файле sw.js и может быть выполнено после успешной регистрации. Это единственный фрагмент кода Service Worker, который находится внутри файла app.js; все остальное, относящееся к Service Worker, записывается в самом файле sw.js.

Жизненный цикл сервис-воркера

После завершения регистрации файл sw.js автоматически загружается, затем устанавливается и, наконец, активируется.

Установка

API позволяет нам добавлять прослушиватели событий для ключевых событий, которые нас интересуют — первое — это событие install:

self.addEventListener('install', (e) => {
    console.log('[Service Worker] Install');
});

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

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

var cacheName = 'js13kPWA-v1';
var appShellFiles = [
  '/pwa-examples/js13kpwa/',
  '/pwa-examples/js13kpwa/index.html',
  '/pwa-examples/js13kpwa/app.js',
  '/pwa-examples/js13kpwa/style.css',
  '/pwa-examples/js13kpwa/fonts/graduate.eot',
  '/pwa-examples/js13kpwa/fonts/graduate.ttf',
  '/pwa-examples/js13kpwa/fonts/graduate.woff',
  '/pwa-examples/js13kpwa/favicon.ico',
  '/pwa-examples/js13kpwa/img/js13kgames.png',
  '/pwa-examples/js13kpwa/img/bg.png',
  '/pwa-examples/js13kpwa/icons/icon-32.png',
  '/pwa-examples/js13kpwa/icons/icon-64.png',
  '/pwa-examples/js13kpwa/icons/icon-96.png',
  '/pwa-examples/js13kpwa/icons/icon-128.png',
  '/pwa-examples/js13kpwa/icons/icon-168.png',
  '/pwa-examples/js13kpwa/icons/icon-192.png',
  '/pwa-examples/js13kpwa/icons/icon-256.png',
  '/pwa-examples/js13kpwa/icons/icon-512.png'
];

Далее во втором массиве генерируются ссылки на изображения для загрузки вместе с контентом из файла data/games.js. После этого оба массива объединяются с помощью функции Array.prototype.concat().

var gamesImages = [];
for(var i=0; i<games.length; i++) {
  gamesImages.push('data/img/'+games[i].slug+'.jpg');
}
var contentToCache = appShellFiles.concat(gamesImages);

Затем мы можем управлять самим событием install:

self.addEventListener('install', (e) => {
  console.log('[Service Worker] Install');
  e.waitUntil(
    caches.open(cacheName).then((cache) => {
          console.log('[Service Worker] Caching all: app shell and content');
      return cache.addAll(contentToCache);
    })
  );
});

Здесь нужно объяснить две вещи: что делает ExtendableEvent.waitUntil и что представляет собой объект caches.

Service Worker не устанавливается до тех пор, пока не будет выполнен код внутри waitUntil. Возвращает обещание — такой подход нужен, потому что установка может занять некоторое время, поэтому нам нужно дождаться ее завершения.

caches — это специальный CacheStorage объект, доступный в рамках данного Service Worker для включения возможности сохранения данных — сохранение в веб-хранилище не будет работать, потому что веб-хранилище синхронно. Вместо этого с Service Workers мы используем Cache API.

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

Активация

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

Ответ на получение

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

self.addEventListener('fetch', (e) => {
    console.log('[Service Worker] Fetched resource '+e.request.url);
});

Ответом может быть что угодно: запрошенный файл, его кешированная копия или фрагмент кода JavaScript, который будет делать что-то конкретное — возможности безграничны.

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

self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.match(e.request).then((r) => {
          console.log('[Service Worker] Fetching resource: '+e.request.url);
      return r || fetch(e.request).then((response) => {
                return caches.open(cacheName).then((cache) => {
          console.log('[Service Worker] Caching new resource: '+e.request.url);
          cache.put(e.request, response.clone());
          return response;
        });
      });
    })
  );
});

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

Метод FetchEvent.respondWith берет на себя управление — это часть, которая функционирует как прокси-сервер между приложением и сетью. Это позволяет нам отвечать на каждый отдельный запрос любым ответом, который мы хотим: подготовленным Service Worker, взятым из кеша, измененным при необходимости.

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

Обновления

Остается еще один вопрос: как обновить Service Worker, когда доступна новая версия приложения, содержащая новые активы? Ключом к этому является номер версии в имени кеша:

var cacheName = 'js13kPWA-v1';

Когда это обновится до версии 2, мы сможем добавить все наши файлы (включая наши новые файлы) в новый кеш:

contentToCache.push('/pwa-examples/js13kpwa/icons/icon-32.png');
// ...
self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open('js13kPWA-v2').then((cache) => {
      return cache.addAll(contentToCache);
    })
  );
});

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

Очистка кеша

Помните событие activate, которое мы пропустили? Его можно использовать для очистки старого кеша, который нам больше не нужен:

self.addEventListener('activate', (e) => {
  e.waitUntil(
    caches.keys().then((keyList) => {
          return Promise.all(keyList.map((key) => {
        if(key !== cacheName) {
          return caches.delete(key);
        }
      }));
    })
  );
});

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

Другие варианты использования

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

Резюме

В этой статье мы кратко рассмотрели, как заставить PWA работать в автономном режиме с помощью сервис-воркеров. Узнайте больше о концепциях, лежащих в основе Service Worker API, и о том, как его использовать более подробно.

Чтобы узнать о различиях между асинхронным и синхронным Javascript, посетите: https://medium.com/@ankitkamboj18/is-javascript-synchronous-or-asynchronous-what-the-hell-is-a-promise-302ee008dfcd

для получения дополнительной информации следуйте: https://medium.com/@ankitkamboj18