Двусторонняя связь с сервис-воркерами

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

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

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

В этом руководстве мы рассмотрим различные способы реализации двусторонней связи между контекстом Window и сервис-воркером, изучив различные API, библиотеку Workbox, а также некоторые сложные случаи.

Диаграмма, на которой показан обмен сообщениями между сервис-воркером и страницей.

Использование Workbox

workbox-window – это набор модулей библиотеки Workbox, предназначенных для запуска в контексте окна. Класс Workbox предоставляет метод messageSW(), который позволяет отправить сообщение зарегистрированному сервис-воркеру экземпляра и дождаться ответа.

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

const wb = new Workbox('/sw.js');
wb.register();

const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);

Сервис-воркер реализует прослушиватель сообщений на другом конце и отвечает зарегистрированному сервис-воркеру:

const SW_VERSION = '1.0.0';

self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

Библиотека использует API браузера, который мы рассмотрим в следующем разделе: Message Channel, но абстрагирует многие детали реализации, упрощая его использование и одновременно используя широкую поддержку браузеров, которую имеет этот API.

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

Использование API браузера

Если библиотеки Workbox недостаточно для ваших нужд, вы можете использовать несколько низкоуровневых API, чтобы реализовать двусторонний обмен данными между страницами и сервис-воркерами. У них есть некоторые сходства и различия:

Сходства:

  • Во всех случаях обмен данными начинается на одном конце через интерфейс postMessage() и принимается на другом конце путем реализации обработчика message.
  • На практике все доступные API позволяют реализовать одни и те же сценарии использования, но некоторые из них могут упростить разработку в определенных сценариях.

Различия:

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

Broadcast Channel API

Browser Support

  • Chrome: 54.
  • Edge: 79.
  • Firefox: 38.
  • Safari: 15.4.

Source

Broadcast Channel API позволяет осуществлять базовый обмен данными между контекстами просмотра с помощью объектов BroadcastChannel.

Чтобы реализовать его, сначала каждый контекст должен создать экземпляр объекта BroadcastChannel с одним и тем же идентификатором и отправлять и получать сообщения от него:

const broadcast = new BroadcastChannel('channel-123');

Объект BroadcastChannel предоставляет интерфейс postMessage() для отправки сообщения в любой контекст прослушивания:

//send message
broadcast.postMessage({ type: 'MSG_ID', });

Любой контекст браузера может прослушивать сообщения с помощью метода onmessage объекта BroadcastChannel:

//listen to messages
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process message...
  }
};

Как видите, явной ссылки на определенный контекст нет, поэтому не нужно сначала получать ссылку на сервис-воркер или определенный клиент.

Диаграмма, показывающая двустороннюю связь между страницей и сервис-воркером с помощью объекта Broadcast Channel.

Недостаток заключается в том, что на момент написания этой статьи API поддерживается в Chrome, Firefox и Edge, но в других браузерах, например Safari, поддержка пока не реализована.

Client API

Browser Support

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1.

Source

Client API позволяет получить ссылку на все объекты WindowClient, представляющие активные вкладки, которыми управляет сервис-воркер.

Поскольку страница управляется одним сервис-воркером, она прослушивает и отправляет сообщения активному сервис-воркеру напрямую через интерфейс serviceWorker:

//send message
navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
});

//listen to messages
navigator.serviceWorker.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process response
  }
};

Аналогичным образом сервисный работник прослушивает сообщения, реализуя прослушиватель onmessage:

//listen to messages
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //Process message
  }
});

Чтобы связаться с любым из своих клиентов, сервис-воркер получает массив объектов WindowClient, выполняя такие методы, как Clients.matchAll() и Clients.get(). Затем оно может postMessage() любой из них:

//Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
  if (clients && clients.length) {
    //Respond to last focused tab
    clients[0].postMessage({type: 'MSG_ID'});
  }
});
Диаграмма, на которой показано, как сервисный работник взаимодействует с массивом клиентов.

Client API – хороший вариант для простой связи со всеми активными вкладками из сервисного воркера. API поддерживается всеми основными браузерами, но не все его методы могут быть доступны. Поэтому перед внедрением API на сайте проверьте, поддерживается ли он браузером.

Канал для сообщений

Browser Support

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41.
  • Safari: 5.

Source

Для Message Channel требуется определить и передать порт из одного контекста в другой, чтобы установить двусторонний канал связи.

Чтобы инициализировать канал, страница создает экземпляр объекта MessageChannel и использует его для отправки порта зарегистрированному сервисному работнику. На странице также реализован прослушиватель onmessage, который получает сообщения из другого контекста:

const messageChannel = new MessageChannel();

//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
  messageChannel.port2,
]);

//Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};
Диаграмма, показывающая, как страница передает порт сервисному работнику для установления двусторонней связи.

Сервисный работник получает порт, сохраняет ссылку на него и использует ее для отправки сообщения на другую сторону:

let communicationPort;

//Save reference to port
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PORT_INITIALIZATION') {
    communicationPort = event.ports[0];
  }
});

//Send messages
communicationPort.postMessage({type: 'MSG_ID'});

MessageChannel поддерживается всеми основными браузерами.

Расширенные API: фоновая синхронизация и фоновое извлечение

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

Фоновая синхронизация

Browser Support

  • Chrome: 49.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

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

Вместо интерфейса postMessage() страница регистрирует sync:

navigator.serviceWorker.ready.then(function (swRegistration) {
  return swRegistration.sync.register('myFirstSync');
});

Затем сервис-воркер прослушивает событие sync, чтобы обработать сообщение:

self.addEventListener('sync', function (event) {
  if (event.tag == 'myFirstSync') {
    event.waitUntil(doSomeStuff());
  }
});

Функция doSomeStuff() должна возвращать объект Promise, указывающий на успешное или неуспешное выполнение действия. Если объект Promise выполняется, синхронизация завершена. Если синхронизация не удастся, будет запланирована повторная попытка. Повторные попытки синхронизации также выполняются при наличии подключения и с экспоненциальной задержкой.

После выполнения операции сервис-воркер может связаться со страницей, чтобы обновить пользовательский интерфейс, используя любой из API связи, рассмотренных ранее.

Google Поиск использует фоновую синхронизацию, чтобы сохранять запросы, которые не удалось выполнить из-за плохого подключения, и повторять их, когда пользователь снова в сети. После выполнения операции они сообщают пользователю о результате с помощью push-уведомления из веб-приложения:

Диаграмма, показывающая, как страница передает порт сервисному работнику для установления двусторонней связи.

Фоновое извлечение

Browser Support

  • Chrome: 74.
  • Edge: 79.
  • Firefox: not supported.
  • Safari: not supported.

Source

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

Background Fetch API позволяет переносить длительные задачи, например скачивание фильмов, подкастов или уровней игры, в сервис-воркер.

Чтобы связаться с сервис-воркером со страницы, используйте backgroundFetch.fetch вместо postMessage():

navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.fetch(
    'my-fetch',
    ['/ep-5.mp3', 'ep-5-artwork.jpg'],
    {
      title: 'Episode 5: Interesting things.',
      icons: [
        {
          sizes: '300x300',
          src: '/ep-5-icon.png',
          type: 'image/png',
        },
      ],
      downloadTotal: 60 * 1024 * 1024,
    },
  );
});

Объект BackgroundFetchRegistration позволяет странице прослушивать событие progress, чтобы отслеживать ход скачивания:

bgFetch.addEventListener('progress', () => {
  // If we didn't provide a total, we can't provide a %.
  if (!bgFetch.downloadTotal) return;

  const percent = Math.round(
    (bgFetch.downloaded / bgFetch.downloadTotal) * 100,
  );
  console.log(`Download progress: ${percent}%`);
});
Диаграмма, показывающая, как страница передает порт сервисному работнику для установления двусторонней связи.
Интерфейс обновляется, чтобы показать прогресс скачивания (слева). Благодаря сервис-воркерам операция может продолжаться, даже если все вкладки закрыты (справа).

Дальнейшие действия

В этом руководстве мы рассмотрели наиболее общий случай взаимодействия между страницей и сервис-воркерами (двунаправленное взаимодействие).

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