Дата публикации:

Прочитал статью “Визуализация работы сервис-воркеров” и почувствовал необходимость разобраться с основами работы service worker’ов, тем более, что как раз занимаюсь этой темой.

Регистрация service worker’а

Регистрация происходит на главной странице web-приложения:

navigator.serviceWorker
  .register('sw.js', {scope})
  .then(onSuccess, onFail);

Простой service worker

Самый простой service worker просто сообщает о факте своей загрузке (файл sw.js):

console.log(
  "SW: registering itself, state:",
  self.serviceWorker.state
);

Состояние worker’а берётся из собственного контекста — self. Если worker продолжительное время неактивен, то браузер завершает поток, в котором он существует, и создаёт новый, как только в worker’е вновь возникает необходимость.

Service Worker States

Всего у service worker’а может быть 6 состояний:

Для трассировки изменения состояний worker’а сразу же после регистрации добавим к нему обработчик событий смены состояния worker’а:

// onSuccess:
(reg) => {
  // get service worker from registration
  const sw = reg.installing;
  console.log("SW registration done, state:", sw.state);
  // setup handler to "statechange" event to trace changes
  sw.addEventListener("statechange", (e) => {
    console.log("State is changed:", e.target.state);
    if (e.target.state === "activated") {
      console.log("uninstall SW after activation.");
      reg.unregister();
    }
  });
}

Код можно посмотреть на github’е. В итоге получаем примерно такой вывод в консоли браузера:

index.html:12 before SW registration.
index.html:30 after SW registration.
index.html:44 end of body.
sw.js:1 registering itself, state: parsed
index.html:18 SW registration done, state: installing
index.html:21 SW state is changed: installed
index.html:21 SW state is changed: activating
index.html:21 SW state is changed: activated
index.html:23 uninstall service worker after activation.
index.html:21 SW state is changed: redundant

Так как в обработчике после перехода в рабочее состояние “activated” вызывается де-регистрация service worker’а, то получается полный цикл всех возможных состояний:

Image 2

Если же убрать де-регистрацию service worker’а после перехода в рабочее состояние:

sw.addEventListener("statechange", (e) => {
  console.log("State is changed:", e.target.state);
  // if (e.target.state === "activated") {
  //   console.log("Uninstall SW after activation.");
  //   reg.unregister();
  // }
});

То вывод на консоль для первой загрузки выглядит так:

index.html:12 before SW registration.
index.html:30 after SW registration.
index.html:44 end of body.
sw.js:1 registering itself, state: parsed
index.html:18 SW registration done, state: installing
index.html:21 SW state is changed: installed
index.html:21 SW state is changed: activating
index.html:21 SW state is changed: activated

Service worker доходит до своего рабочего состояния (“activated”) и остаётся в нём. При повторной загрузке страницы вывод на консоль уже такой:

index.html:12 before SW registration.
index.html:30 after SW registration.
index.html:44 end of body.
index.html:18 SW registration done, state: activated

Правда для этого нужно изменить строку:

// const sw = reg.installing;
const sw = reg.installing ?? reg.active;

Так как при попытке повторной регистрации того же самого service worker’а возвращается уже установленный (active).

Перевести service worker в состояние “redundant” можно вручную, через панель инструментов разработчика:

Image 3

Service Worker Events

Основным назначением service worker’а является кэширование удалённого контента для ускорения работы пользователя web-приложения и обеспечения работоспособности web-приложения при потере соединения с интернетом (в offline-режиме).

Как правило, регистрацию service worker’а откладывают до завершения загрузки головной страницы web-приложения (index.html), чтобы загрузка кэша worker’ом не влияла на загрузку головной страницы:

window.addEventListener('load', function () {
  navigator.serviceWorker.register('sw.js')
    .then()
    .catch();
});

В самом service worker’е обычно вешают обработчики на события для выполнения следующих типовых задач:

Есть ещё такие события:

install

Как правило обработчик события install заполняет кэш service worker’а данными из сети:

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open("static-v2")
      .then((cache) => {
        return cache.addAll([
          "./index.html",
          "./pwa.webmanifest"
        ]);
      })
      .then(() => {
        console.log("Cache is loaded.");
      })
  );
});

Диспетчер событий ожидает выполнения промиса, переданного в event.waitUntil, и только после загрузки кэша переводит service worker из состояния installing в installed.

activate

Этот обработчик удаляет из кэша данные, относящиеся к предыдущей версии service worker’а

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          // delete all caches except "static-v2"
          if (cacheName !== "static-v2") {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Вообще-то правила работы с кэшем в web-приложении могут быть довольно разнообразны. В примерах, как правило, приводится самый простой вариант: при регистрации заполнили новый кэш, при активации — удалили старый. Но service worker обладает меньшей изменчивостью, чем всё приложение в целом (вернее, отдельные его ресурсы — HTML/CSS/JS). Поэтому web-приложение должно обладать возможностью принудительной ре-инициализации кэша (например, при переходе по ссылке /sw/cache/reset) без переинсталляции service worker’а.

fetch

Опять-таки, вот один из самых простых вариантов — возвращаем ресурс из кэша, если он там есть, или запрашиваем из сети:

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then(response => {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});

Примеры кода также выложены на github.

Обновление основной страницы

У service worker’а есть особенность — он не обрабатывает запросы той страницы, которая запросила его регистрацию. По-умолчанию service worker обработает все fetch-запросы только после перезагрузки страницы (reload’а).

При необходимости можно сообщить браузеру, что service worker готов к выполнениюfetch-запросов регистрационной страницы через:

await clients.claim();

Или просто повесить обработчик на событие смены статуса service worker’а в index.html:

window.addEventListener('load', function () {
  navigator.serviceWorker.register('sw.js')
    .then((reg) => {
      const sw = reg.installing
        ? reg.installing
        : reg.waiting
          ? reg.waiting
          : reg.active;
      sw.addEventListener("statechange", (e) => {
        console.log("State is changed:", e.target.state);
        if (e.target.state === "activated") {
          location.reload();
        }
      });
    });
});

и принудительно обновить страницу после того, как service worker будет готов к обработке запросов.

Вот так выглядят запросы к сети с точки зрения браузера в демо-приложении (второй вариант, перезагрузка через location.reload):

Image 4

Материалов о service worker’ах в Сети довольно много, но большинство — как и эта публикация — рассматривает самый простой сценарий. В реальном приложении требования к service worker’у обычно заметно сложнее.

Для более глубокого понимания лучше обратиться к детальному описанию API:

написать свою собственную статью или, что лучше всего, внедрить service worker в реальное приложение.

Service Worker показывает один из способов, которым браузер поддерживает автономное поведение приложения. Общая картина исполнения, состояния и жизненных циклов собрана в книге «Браузер как операционная система для разработки современных приложений».