Прочитал я хорошую статью "Обновление вашего PWA в продакшене" и задался вопросом - а как часто при обновлении PWA нужно обновлять непосредственно сам service worker? Ведь что такое service worker по сути? "Прокладка" (прокси) между приложением, работающим в браузере, и внешними серверами, с которых это приложение тянет нужные ему ресурсы. По большому счёту, функционал service worker'а сводится к некоторому набору стратегий и пониманию того, к какому ресурсу какую стратегию применять и когда (я сейчас не рассматриваю push notifications и background sync, но изложенное в какой-то степени применимо и к ним).
То есть, код service worker'а более стабилен по сравнению с кодом приложения и во многих случаях для его "обновления" достаточно программно обнулить кэш-хранилище браузера и обновить "понимание того, к какому ресурсу какую стратегию применять" - обновить конфигурацию service worker'а. А для этого нужно приложению нужно иметь возможность каким-то образом управлять состоянием service worker'а и передавать ему данные, что осложняется тем, что приложение и service worker работают в различных потоках.
Под катом пример того, каким образом можно настроить управление service worker'ом из основного приложения при помощи Channel Messaging API.
В русскоязычной среде нет устоявшегося аналога английского service worker. В тексте данной публикации я буду использовать словосочетание "сервис-служба" в качестве перевода для "service worker" просто для того, чтобы реже переключать раскладку клавиатуры, пусть даже оно и звучит как "масляное масло". Появится лучший перевод - буду использовать его.
Принцип взаимодействия
Код сервис-службы выполняется в отдельном потоке и является событийно-ориентированным по своей архитектуре (т.е., полностью асинхронным). Поэтому общение приложения с сервис-службой также асинхронно - обе стороны подписываются на событие message
, а затем каким-то образом реагируют на получаемые сообщения и отправляют свои. Схематически это можно отобразить таким образом:
Приложение, на своей стороне, формирует сообщение к сервис-службе и регистрирует контекст сообщения, включающий (при необходимости) callback-функцию, которая обработает результат, планируемый к получению в будущем от сервис-службы.
Приложение отправляет сообщение сервис-службе в соседний поток.
Сервис-служба получает сообщение и выполняет запрошенную операцию (например, изменяет конфигурацию или обновляет кэш).
После выполнения операции сервис-служба отправляет приложению обратное сообщение.
Приложение получает сообщение от сервис-службы и находит в реестре контекст, соответствующий полученному сообщению.
Приложение передаёт в контекст данные, полученные от сервис-службы, и выполняет (при необходимости) callback-функцию.
Базовый код
Приложение
Отправка сообщения активной сервис-службе:
const sw = await navigator.serviceWorker.ready;
if (sw.active) sw.active.postMessage(msg);
Подписка на сообщения от сервис-службы:
navigator.serviceWorker.addEventListener('message', onMessage);
Сервис-служба
Отправка в приложение обратного сообщения осуществляется в обработчике события:
function onMessage(event) {
//...
event.source.postMessage(msg);
}
Подписка на сообщения от приложения:
self.addEventListener('message', onMessage);
Демо
Для демонстрации управления сервис-службой я сделал приложение:
Для корректной работы приложения должно быть отключено собственное кэширование в браузере (в Chrome - панель инструментов, вкладка
Network
,Disable cache
), в противном случае повторные запросы на картинки даже не доходят до сервис-службы.
Приложение по очереди загружает два изображения: первичное и вторичное. Первичное изображение (./img/primary.svg
) всегда содержит картинку лошади, а вторичное (./img/secondary.svg
) - картинку кошки или собаки, в зависимости от состояния сервис-службы:
Замещение изображения происходит по кнопке "Заместить":
Переключение сервис-службы происходит в режиме просмотра первичного изображения при помощи радио-кнопки. Вот лог в консоли браузера, отображающий процессы получения текущего состояния сервис-службы (для инициализации радио-кнопки) и однократного переключения состояния:
Состояние сервис-службы сохраняется в переменной _useCat
, изменения отслеживаются через отладчик:
Структура сообщения
DTO-объект для передачи информации от приложения в сервис-службу и обратно:
class Dto {
id;
payload;
type;
}
В демо-приложении возможны два типа сообщений: get_state
и set_state
.
Контроллер
Контроллер обеспечивает синхронизацию отправки сообщений сервис-службе и сопоставление отправленным сообщениям обратных сообщений от сервис-службы. Приложение может использовать команды контроллера (getState()
, setState(replaceWithCat)
) для управления сервис-службой:
class Controller {
constructor(spec) {
const _queue = {};
const generateMsgId = () => `${(new Date()).getTime()}`;
this.getState = function () {};
this.setState = function (replaceWithCat = true) {};
function onMessage(event) {}
self.navigator.serviceWorker
.addEventListener('message', onMessage);
}
}
Вот основа кода регистрации сообщения в реестре и отправки сообщения сервис-службе:
this.setState = function (replaceWithCat = true) {
const id = generateMsgId();
return new Promise(async (resolve) => {
_queue[id] = resolve;
const msg = new Dto();
msg.id = id;
//...
const sw = await navigator.serviceWorker.ready;
sw.active.postMessage(msg);
});
};
А это код получения обратного сообщения от сервис-службы и разрешения зарегистрированного ранее промиса:
function onMessage(event) {
const id = event.data.id;
const payload = event.data.payload;
if (typeof _queue[id] === 'function') _queue[id](payload);
}
Сервис-служба
В сервис-службе весь функционал умещается в одном обработчике:
function onMessage(event) {
const msg = event.data;
const res = {};
res.id = msg.id;
if (msg.type === 'get_state') {...}
else if (msg.type === 'set_state') {...}
event.source.postMessage(res);
}
Обработчик принимает сообщение, выполняет запрошенную операцию и возвращает результат вместе с id
операции обратным сообщением.
Приложение
Приложение общается через контроллер, поэтому тут совсем всё тривиально:
const isCat = await swControl.getState();
Резюме
Если функционал сервис-службы отделить от её конфигурации (список ресурсов и стратегии кэширования), то обновления самой сервис-службы нужно будет делать реже.
Функционал по зачистке кэша, используемого сервис-службой, лучше ставить до вывода приложения в прод, чтобы избежать ситуации "установил ServiceWorker - пора менять домен".
В силу асинхронной природы сервис-службы для управления её состоянием из приложения приходится использовать асинхронный подход (при этом данные "туда" и "обратно" передаются по-значению, а не по ссылке).
Добавление промежуточного контроллера, завязанного по управляющему функционалу на возможности сервис-службы, позволяют замаскировать асинхронную природу связи для остального приложения (сопоставление ответа запросу делается в контроллере).
Использование отладчика в браузере очень сильно способствует пониманию процессов, происходящих внутри программы.