Двусторонняя связь с работниками службы

Эндрю Гуан
Andrew Guan
Демиан Рензулли
Demián Renzulli

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

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

В этом руководстве мы рассмотрим различные способы реализации двусторонней связи между контекстом окна и Service Worker , изучив различные 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 браузера, который мы рассмотрим в следующем разделе: Канал сообщений , но абстрагирует многие детали реализации, что упрощает ее использование, используя при этом широкую поддержку этого API браузерами .

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

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

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

Сходства:

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

Различия:

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

API вещательного канала

Browser Support

  • Хром: 54.
  • Край: 79.
  • Firefox: 38.
  • Сафари: 15.4.

Source

API Broadcast Channel обеспечивает базовую связь между контекстами просмотра через объекты 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, пока его не поддерживают .

Клиентский API

Browser Support

  • Хром: 40.
  • Край: 17.
  • Firefox: 44.
  • Сафари: 11.1.

Source

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

Поскольку страница контролируется одним сервисным работником, она прослушивает и отправляет сообщения активному сервисному работнику напрямую через интерфейс 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
  }
});

Чтобы связаться с любым из своих клиентов, service worker получает массив объектов 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 поддерживается всеми основными браузерами , но не все его методы могут быть доступны, поэтому обязательно проверьте поддержку браузера перед его реализацией на своем сайте.

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

Browser Support

  • Хром: 2.
  • Край: 12.
  • Firefox: 41.
  • Сафари: 5.

Source

Канал сообщений требует определения и передачи порта из одного контекста в другой для установления двустороннего канала связи.

Для инициализации канала страница создает экземпляр объекта 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

  • Хром: 49.
  • Край: 79.
  • Firefox: не поддерживается.
  • Safari: не поддерживается.

Source

Чат-приложению может потребоваться убедиться, что сообщения никогда не теряются из-за плохого соединения. 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() должна возвращать обещание, указывающее на успешность/неудачу того, что она пытается сделать. Если оно выполняется, синхронизация завершена. Если она не выполняется, будет запланирована еще одна синхронизация для повторной попытки. Повторные синхронизации также ждут подключения и используют экспоненциальную задержку.

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

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

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

Фоновая выборка

Browser Support

  • Хром: 74.
  • Край: 79.
  • Firefox: не поддерживается.
  • Safari: не поддерживается.

Source

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

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}%`);
});
Диаграмма, показывающая передачу страницы порту сервисному работнику для установления двусторонней связи.
Пользовательский интерфейс обновляется, чтобы отобразить ход загрузки (слева). Благодаря сервис-воркерам операция может продолжаться, когда все вкладки закрыты (справа).

Следующие шаги

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

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