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

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

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

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

В этом руководстве мы рассмотрим различные способы реализации двусторонней связи между контекстом окна и работника сервиса , изучая различные 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
  }
});

Для обратной связи с любым из своих клиентов сервис-воркер получает массив объектов 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

  • Хром: 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-адресов для кэширования, рассмотренные ранее варианты являются хорошим выбором. Если задача выполняется слишком долго, браузер завершит работу сервис-воркера, в противном случае это создаст риск для конфиденциальности пользователя и разрядки батареи.

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

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

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

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