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

Жизненный цикл сервисного работника – самая сложная его часть. Если вы не знаете, что он пытается сделать и в чем его преимущества, может показаться, что он борется с вами. Но как только вы поймете, как это работает, вы сможете предоставлять пользователям бесперебойные и ненавязчивые обновления, сочетая лучшие веб- и нативные шаблоны.

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

Намерение

Цели жизненного цикла:

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

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

Первый сервис-воркер

Вкратце:

  • Событие install – первое событие, которое получает сервис-воркер, и оно происходит только один раз.
  • Объект Promise, переданный в installEvent.waitUntil(), сообщает о продолжительности и успешности установки.
  • Сервис-воркер не будет получать события, такие как fetch и push, пока не будет успешно установлен и не станет активным.
  • По умолчанию запросы на получение данных со страницы не проходят через сервис-воркер, если только сам запрос страницы не прошел через него. Поэтому, чтобы увидеть результаты работы сервисного работника, вам нужно будет обновить страницу.
  • clients.claim() может переопределить это значение по умолчанию и взять под контроль страницы, которые не контролируются.

Возьмите этот HTML-код:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Он регистрирует сервис-воркер и через три секунды добавляет изображение собаки.

Вот его сервисный скрипт (sw.js):

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

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

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

Он кеширует изображение кота и показывает его каждый раз, когда поступает запрос на /dog.svg. Однако если вы запустите приведенный выше пример, то при первой загрузке страницы увидите собаку. Обновите страницу, и вы увидите кота.

Область действия и управление

По умолчанию область действия регистрации сервисного работника – ./ относительно URL скрипта. Это означает, что если вы зарегистрируете сервис-воркер в //example.com/foo/bar.js, то по умолчанию он будет работать в области //example.com/foo/.

Страницы, воркеры и общие воркеры называются clients. Сервис-воркер может управлять только клиентами, которые находятся в области действия. Когда клиент находится под управлением, его запросы на получение данных проходят через сервис-воркер в области видимости. Вы можете определить, управляется ли клиент, с помощью navigator.serviceWorker.controller, который будет иметь значение null или экземпляр сервис-воркера.

Скачивание, синтаксический анализ и выполнение

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

Инструменты разработчика Chrome показывают ошибку в консоли и в разделе сервисного работника на вкладке приложения:

Ошибка, показанная на вкладке инструментов разработчика для сервисного кеша

Установить

Первое событие, которое получает сервисный работник, – install. Оно активируется, как только сервис-воркер начинает работу, и вызывается только один раз для каждого сервис-воркера. Если вы измените скрипт сервис-воркера, браузер будет считать его другим сервис-воркером и он получит собственное событие install. Подробнее об обновлениях мы поговорим позже.

Событие install позволяет кешировать все, что нужно, прежде чем вы сможете управлять клиентами. Объект Promise, который вы передаете в event.waitUntil(), сообщает браузеру, когда установка завершена и была ли она успешной.

Если обещание отклонено, это означает, что установка не удалась и браузер удаляет сервис-воркер. Он никогда не будет управлять клиентами. Это означает, что мы можем рассчитывать на то, что cat.svg будет присутствовать в кеше в наших событиях fetch. Это зависимость.

Активировать

Когда сервисный работник будет готов управлять клиентами и обрабатывать функциональные события, такие как push и sync, вы получите событие activate. Однако это не означает, что страница, на которой вызывается .register(), будет контролироваться.

При первой загрузке демоверсии, несмотря на то, что dog.svg запрашивается спустя долгое время после активации сервис-воркера, он не обрабатывает запрос, и вы по-прежнему видите изображение собаки. По умолчанию используется значение consistency. Если страница загружается без сервисного скрипта, то и ее подресурсы будут загружаться без него. Если вы загрузите демоверсию во второй раз (то есть обновите страницу), она будет контролироваться. Страница и изображение пройдут через fetch событий, и вы увидите кота.

clients.claim

Вы можете взять под контроль неконтролируемых клиентов, вызвав clients.claim() в сервис-воркере после его активации.

Вот вариант приведенного выше примера, в котором clients.claim() вызывается в событии activate. В первый раз вы должны увидеть кота. Я говорю "должен", потому что это зависит от времени. Вы увидите кота, только если сервис-воркер активируется и clients.claim() вступит в силу до того, как изображение попытается загрузиться.

Если вы используете сервис-воркер, чтобы загружать страницы не так, как они загружаются через сеть, clients.claim() может вызвать проблемы, поскольку сервис-воркер будет управлять некоторыми клиентами, которые загрузились без него.

Обновление сервисного работника

Вкратце:

  • Обновление запускается в следующих случаях:
    • Переход на страницу, входящую в область действия.
    • Функциональные события, такие как push и sync, если в течение последних 24 часов не выполнялась проверка обновлений.
    • Вызов .register() только в том случае, если URL сервисного работника изменился. Однако изменять URL воркера не рекомендуется.
  • Большинство браузеров, включая Chrome 68 и более поздние версии, по умолчанию игнорируют заголовки кеширования при проверке обновлений зарегистрированного скрипта service worker. Они по-прежнему учитывают заголовки кеширования при получении ресурсов, загруженных в сервис-воркер через importScripts(). Вы можете переопределить этот режим работы по умолчанию, задав параметр updateViaCache при регистрации сервисного работника.
  • Сервис-воркер считается обновленным, если он отличается от уже имеющегося в браузере. (Мы расширяем это правило, чтобы оно распространялось и на импортированные скрипты/модули.)
  • Обновленный сервис-воркер запускается вместе с существующим и получает собственное событие install.
  • Если новый обработчик возвращает код статуса, отличный от "OK" (например, 404), не может выполнить синтаксический анализ, выдает ошибку во время выполнения или отклоняется во время установки, он удаляется, а текущий обработчик остается активным.
  • После успешной установки обновленный рабочий процесс будет wait, пока существующий рабочий процесс не будет контролировать ноль клиентов. (Обратите внимание, что при обновлении клиенты перекрываются.)
  • self.skipWaiting() предотвращает ожидание, то есть сервис-воркер активируется сразу после установки.

Допустим, мы изменили скрипт сервисного работника, чтобы он отвечал изображением лошади, а не кошки:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

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

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

Посмотрите демонстрацию. Вы все равно должны увидеть изображение кошки. Вот почему…

Установить

Обратите внимание, что я изменил название кеша с static-v1 на static-v2. Это означает, что я могу настроить новый кеш, не перезаписывая данные в текущем, который все ещё используется старым сервис-воркером.

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

Ожидание

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

Если вы запустили обновленную демоверсию, вы все равно должны увидеть изображение кота, потому что воркер V2 ещё не активирован. Вы можете увидеть новый сервисный работник, ожидающий на вкладке «Приложение» в DevTools:

Инструменты разработчика, показывающие, что новый сервис-воркер ожидает

Даже если у вас открыта только одна вкладка с демоверсией, обновления страницы недостаточно, чтобы запустить новую версию. Это связано с тем, как работает навигация в браузере. При переходе текущая страница не исчезает, пока не будут получены заголовки ответа. Даже после этого она может остаться, если в ответе есть заголовок Content-Disposition. Из-за этого перекрытия текущий сервис-воркер всегда управляет клиентом во время обновления.

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

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

Активировать

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

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

Если вы передадите промис в event.waitUntil(), он будет буферизовать функциональные события (fetch, push, sync и т. д.), пока промис не будет разрешен. Поэтому, когда активируется событие fetch, активация полностью завершена.

Пропустить этап ожидания

Эта фаза означает, что у вас одновременно работает только одна версия сайта. Если вам это не нужно, вы можете ускорить активацию нового сервис-воркера, вызвав self.skipWaiting().

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

Не имеет значения, когда вы вызываете skipWaiting(), главное – сделать это до или во время ожидания. Обычно он вызывается в событии install:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

Но вы можете вызвать его в результате postMessage() к сервисному работнику. Например, вы хотите skipWaiting() после взаимодействия с пользователем.

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

Обновления вручную

Как упоминалось ранее, браузер автоматически проверяет наличие обновлений после навигации и функциональных событий, но вы также можете запустить их вручную:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

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

Не меняйте URL скрипта сервисного работника

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

Это может привести к следующей проблеме:

  1. index.html регистрирует sw-v1.js как сервис-воркер.
  2. sw-v1.js кеширует и обслуживает index.html, поэтому он работает в автономном режиме.
  3. Вы обновляете index.html, чтобы зарегистрировать новый sw-v2.js.

Если вы сделаете это, пользователь никогда не получит sw-v2.js, потому что sw-v1.js будет показывать старую версию index.html из своего кеша. Вы поставили себя в положение, когда вам нужно обновить сервисного работника, чтобы обновить сервисного работника. Фу.

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

Простая разработка

Жизненный цикл сервисного работника построен с учетом интересов пользователя, но во время разработки это немного усложняет задачу. К счастью, есть несколько инструментов, которые могут помочь:

Обновлять при перезагрузке

Это мой любимый.

Инструменты разработчика с включенным параметром &quot;Обновлять при перезагрузке&quot;

Это изменение упрощает работу разработчиков. Каждый переход:

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

Это значит, что вы будете получать обновления при каждом переходе (в том числе при обновлении страницы), и вам не придется перезагружать страницу дважды или закрывать вкладку.

Пропустить ожидание

Инструменты разработчика с выделенной кнопкой &quot;Пропустить ожидание&quot;

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

Shift + Обновить

Если вы принудительно перезагрузите страницу (shift-reload), она полностью обойдет сервис-воркер. Она будет неконтролируемой. Эта функция включена в спецификацию, поэтому она работает в других браузерах, поддерживающих сервис-воркеры.

Как обрабатывать обновления

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

Чтобы обеспечить поддержку как можно большего количества шаблонов, весь цикл обновления можно отслеживать:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

Жизненный цикл продолжается

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