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

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

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

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

Комментарии (3)


  1. monochromer
    11.10.2021 21:39

    В русскоязычной среде нет устоявшегося аналога английского service worker. 

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


    1. monochromer
      01.11.2021 19:32

      "не переводить" - только сейчас заметил


      1. flancer Автор
        01.11.2021 20:33

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