PWA: управление service-worker’ом

от автора

Прочитал я хорошую статью «Обновление вашего 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 — пора менять домен«.

  • В силу асинхронной природы сервис-службы для управления её состоянием из приложения приходится использовать асинхронный подход (при этом данные «туда» и «обратно» передаются по-значению, а не по ссылке).

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/582366/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *