- PVSM.RU - https://www.pvsm.ru -

Некоторые тонкости использования Service Workers

Некоторые тонкости использования Service Workers - 1

Предисловие

Сервис-воркеры (Service Workers, да простят меня читатели) сегодня являются полезным дополнением к основной функциональности сайта: тут и работа в оффлайне, и фоновая синхронизация данных, и модные пуш-уведомления.
Однако большое количество статей про сервис-воркеры выглядят достаточно сжато и описывают простые примеры. Я попробую обратить внимание на некоторые особенности работы сервис-воркеров, так что требуются какие-то базовые знания. Отправной точкой может быть эта статья [1] (перевод [2]) или чуть более подробная статья [3].

Несколько сервис-воркеров на одном домене

У регистрации (registration) конкретного сервис-воркера есть такое понятие, как scope. Оно определяет, какие страницы на определённом домене будут подпадать под её контроль. При этом можно регистрировать несколько сервис-воркеров на одном домене, но с разными scope. Если попробовать зарегистрировать их с разными именами, но одним scope, то установленный позднее воркер будет «замещать» своего более раннего брата.

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

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

Рассмотрим пример: у нас есть установленный сервис-воркер со scope /. Пусть это будет новостной сайт и мы предоставляем оффлайновые версии текстов. Также есть панель управления по пути /admin/ со своим собственным сервис-воркером. Если второй сервис-воркер ещё не попытались установить, то getRegistaration() будет возвращать регистрацию первого сервис-воркера и это может приводить к ошибкам (например, мы будем слать нотификации из панели администратора в сервис-воркер, не готовый к ним вовсе).

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

А если мы не знаем все scope, то есть метод getRegistrations(), который просто возвращает все регистрации с текущего домена в виде массива. Требуется Firefox или Chrome 45+.

Связь между страницей и сервис-воркером

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

Пример на serviceworke.rs [4] показывает простой способ общения с сервис-воркером:

navigator.serviceWorker.controller.postMessage(message.value);

Здесь controller — сервис-воркер, контролирующий страницу. В свежих браузерах (все версии Firefox и Chrome 51+) можно достаточно просто ответить на такой запрос:

self.addEventListener('message', function (event) {
    event.source.postMessage('response');
});

В более старых версиях приходилось обходить все вкладки и находить нужную, а то и создавать руками MessageChannel. Также теперь у нас есть возможность отправлять сообщение вкладке из события fetch. Всё это описано в статье [5], разве что современное апи у нас уже есть.

Другой момент — хранение данных в сервис-воркере. Люди, уже опробовавшие сервис-воркеры, могли заметить, что LocalStorage там нет. Всё потому, что в сервис-воркерах был взят курс на полностью асинхронное апи (за исключением, пожалуй, importScripts [6]). Но внутри всё ещё остаются доступны:

  • caches
  • indexedDB
  • просто переменные, объявленные в контексте воркера (но они недолговечны и будут позабыты при остановке сервис-воркера)

И caches, и indexedDB доступны обычным образом на страницах, полностью разделяя с воркером данные. Если обратиться к предыдущему параграфу, можно также прийти к выводу, что и несколько сервис-воркеров на одном origin будут разделять данные! В таком случае нужно не тереть кеши другого сервис-воркера, например, проверяя их по префиксу:

var CACHE_PREFIX = 'my-page-';
var CACHE_VERSION = 1;
var CACHE_NAME = CACHE_PREFIX + CACHE_VERSION;

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function (cacheNames) {
      return Promise.all(
        cacheNames.map(function (cacheName) {
          if (cacheName.indexOf(CACHE_PREFIX) === 0) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Или как-то так [7], но тогда нужно будет иметь в одном месте полный список возможных кешей.

Но при всём этом стоит помнить, что никто не гарантирует 100% сохранность данных в хранилищах. Браузер может автоматически чистить CacheStorage и indexedDB при нехватке места на диске, да и пользователь может сделать это сам.

Кроссдоменные запросы и прочее взаимодействие с другими доменами

С введением fetch ситуация могла показаться немного запутанной (там есть разные режимы запроса/ответа), а с сервис-воркерами всё становится в два раза сложнее: один fetch на стороне клиента, второй — на стороне сервис-воркера.

Самое простое понимание, к которому можно придти: «обмануть» CORS и получить доступ к контенту с другого домена без заголовков не получится. Важно разделять два вида использования: с доступом со стороны javascript и без него. Например, подменить одну картинку другой можно без проблем: достаточно указать в fetch сервис-воркера mode: 'no-cors' и не важно, какие там заголовки. Если не использовать 'no-cors', fetch будет ожидать CORS заголовки и в случае их отсутствия всё окончится ошибкой.

Если говорить более строго, то любой запрос (Request) со страницы имеет mode. Например, запрос картинки — 'no-cors', а запрос картинки с атрибутом crossOrigin (anonymous или use-credentials) — уже 'cors'. Запросы через XMLHttpRequest всегда в режиме 'cors'. А fetch позволяет задавать режим напрямую.

Ответ (Response) имеет свойство type. Запросы на текущий домен — 'basic'. Иначе, если режим запроса — 'cors', то type ответа тоже будет 'cors', при наличии необходимых заголовков. Режим ответа 'opaque' можно получить на запрос в режиме 'no-cors', в нём нельзя получить доступ к каким-либо данным ответа.

Здесь описаны не все возможные виды режимов запросов, но этого должно быть достаточно для общего понимания. Больше информации можно почерпнуть из статьи с описанием fetch [8].

Теперь попробуем всё скомбинировать. Со страницы уходит запрос, его перехватывает сервис-воркер и делает свой fetch, получает ответ. До текущего момента ситуация разобрана, но теперь будет нюанс: при передаче ответа с типом 'opaque' в ответ на запрос страницы. который был сделан не с режимом 'no-cors', мы получим ошибку.

Помимо просто запросов, мы можем установить сервис-воркер на другой домен. Нет, мы не получим контроль за другой страницей через наш сервис-воркер — условия на сервис-воркер остаются теми же (сам скрипт должен быть на том же домене, на который регистрируется воркер). Для этого можно использовать iframe с нужного домена — разрешений от пользователя не требуется и iframe можно сделать просто невидимым.

Другая интересная возможность, которая сейчас находится в своей ранней версии — Foreign Fetch [9]. Если обычный сервис-воркер контролирует запросы со страницы в своём scope (страница в scope, а не запросы), то foreign fetch позволяет контролировать запросы на свой домен. Допустим, обычное событие fetch будет срабатывать при запросе за библиотекой на CDN, а foreignFetch будет срабатывать при всех запросах за этой библиотекой на любых сайтах! Это любопытная возможность может быть использована, например, службами аналитики.

Тестирование

С написанием тестов на сервис-воркеры есть определённые сложности. Составление теста не так просто: если мы хотим проверить оффлайновый режим, то нужно как-то эмулиовать ошибки сети, если хотим проверить обновление — нужно подменять файл новым и тому подобное.

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

Есть стоящая статья [10] на тему тестирования сервис-воркеров. В ней есть ссылки и на пару инструментов: sw-unit-test-sample [11] и platinum-sw [12] (элемент для Polymer, в нём есть также пара тестов). В статье также описан интересный приём: создание ифрейма для того, чтобы он контролировался тестируемым сервис-воркером. Вообще говоря, у элементов iframe и object есть другая особенность: запросы за ними и их содержимым идут в обход текущего сервис-воркера страницы, используя собственные сервис-воркеры.

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

Важный нюанс при работе автотестов — определение момента, когда сервис-воркер контролирует страницу и может перехватывать запросы. Простой navigator.serviceWorker.ready не всегда является верным решением — ready срабатывает в момент активации сервис воркера, но до того, как закончится выполнение clients.claim(). Более подробно описано здесь [13], как одно из решений — слушать событие controllerchange [14].

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

Есть несколько нюансов при обновлении сервис-воркеров, на которые стоит обратить внимание.

Несмотря на поддержку кеширующих заголовков при запросе скрипта сервис-воркера, браузеры уменьшают время жизни кеша до 24 часов. Сделано это для того, чтобы случайно не оставить сайт у пользователя в убитом состоянии на большой промежуток времени. Вот хороший ответ на StackOverflow [15] про кеширование.

Другой нюанс: обновление срабатывает, только если сам скрипт сервис-воркера обновился, и определение этого происходит побайтово. Из этого следует, что обновление файлов, которые подключены через importScripts, не приведёт к обновлению самого сервис-воркера [16].

При обновлении часто добавляются в кеш из сети какие-то файлы. Но при этом работает браузерный кеш! Как и при вызовах fetch внутри сервис-воркера. Нужно либо быть уверенным, что файлы не поменялись (например, включать версию/хеш в название файла), либо загружать ресурсы в обход кеша. Чтобы загружать ресурсы в обход кеша, можно или руками звать fetch и потом добавлять ответ в кеш (не забывая проверять response.ok, например), или использовать опцию cache: 'no-cache' Request'а (пока работает только в Firefox Nightly). И то и то описано в статье Jake Archibald [17].

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

Разное

Автор: 4eb0da

Источник [24]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/html5/211588

Ссылки в тексте:

[1] эта статья: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

[2] перевод: https://developer.mozilla.org/ru/docs/Web/API/Service_Worker_API/Using_Service_Workers

[3] чуть более подробная статья: https://www.smashingmagazine.com/2016/02/making-a-service-worker/

[4] Пример на serviceworke.rs: https://serviceworke.rs/message-relay.html

[5] статье: https://ponyfoo.com/articles/serviceworker-messagechannel-postmessage

[6] importScripts: https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts

[7] как-то так: https://developers.google.com/web/fundamentals/getting-started/primers/service-workers#update-a-service-worker

[8] статьи с описанием fetch: https://developers.google.com/web/updates/2015/03/introduction-to-fetch

[9] Foreign Fetch: https://developers.google.com/web/updates/2016/09/foreign-fetch

[10] стоящая статья: https://gauntface.com/blog/2015/12/14/unit-testing-service-worker

[11] sw-unit-test-sample: https://github.com/GoogleChrome/sw-unit-test-sample

[12] platinum-sw: https://github.com/PolymerElements/platinum-sw

[13] здесь: https://github.com/w3c/ServiceWorker/issues/799

[14] слушать событие controllerchange: https://github.com/PolymerElements/platinum-sw/blob/master/test/controlled-promise.js

[15] хороший ответ на StackOverflow: http://stackoverflow.com/a/38854905

[16] не приведёт к обновлению самого сервис-воркера: http://blog.pushpad.xyz/2016/07/service-worker-importscripts-never-updates-scripts/

[17] статье Jake Archibald: https://jakearchibald.com/2016/caching-best-practices/#a-service-worker-can-extend-the-life-of-these-bugs

[18] serviceworke.rs — сайт с примерами использования сервис-воркеров: https://serviceworke.rs/

[19] Хорошо описанный процесс жизни сервис-воркера: https://bitsofco.de/the-service-worker-lifecycle/

[20] Пример с префетчем видео и ответом на заголовок ranges: https://samdutton.github.io/samples/service-worker/prefetch-video/

[21] is serviceworker ready? Сайт Jake Archibald со списком разных возможностей, поддержкой браузеров и примерами использования: https://jakearchibald.github.io/isserviceworkerready/

[22] pwa.rocks. Сайт с примерами progressive web apps: https://pwa.rocks/

[23] Примеры использования сервис-воркеров от Google: https://www.chromestatus.com/samples#service

[24] Источник: https://habrahabr.ru/post/315660/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best