В некоторых случаях веб-приложению может потребоваться установить двусторонний канал связи между страницей и сервис-воркером.
Например: в подкасте 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 браузерами .

Использование API браузера
Если библиотеки Workbox недостаточно для ваших нужд, существует несколько низкоуровневых API для реализации «двустороннего» взаимодействия между страницами и сервис-воркерами. У них есть некоторые сходства и различия:
Сходства:
- Во всех случаях общение начинается на одном конце через интерфейс
postMessage()
и принимается на другом конце путем реализации обработчикаmessage
. - На практике все доступные API позволяют нам реализовывать одни и те же варианты использования, но некоторые из них могут упростить разработку в некоторых сценариях.
Различия:
- Они по-разному идентифицируют другую сторону коммуникации: некоторые из них используют явную ссылку на другой контекст, в то время как другие могут общаться неявно через прокси-объект, созданный на каждой стороне.
- Поддержка браузеров различается.

API вещательного канала
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...
}
};
Как видно, нет явной ссылки на конкретный контекст, поэтому нет необходимости сначала получать ссылку на работника сервиса или какого-либо конкретного клиента.

Недостатком является то, что на момент написания этой статьи API поддерживается Chrome, Firefox и Edge, но другие браузеры, такие как Safari, пока его не поддерживают .
Клиентский API
Клиентский 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 на сайте обязательно проверьте поддержку браузерами.
Канал сообщений
Канал сообщений требует определения и передачи порта из одного контекста в другой для установления двустороннего канала связи.
Для инициализации канала страница создаёт экземпляр объекта 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 для обработки конкретных сценариев: отсутствия подключения и длительной загрузки.
Фоновая синхронизация
Приложение чата может быть заинтересовано в том, чтобы сообщения никогда не терялись из-за плохого соединения. 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-уведомление:

Фоновая выборка
Для относительно коротких задач, таких как отправка сообщения или списка 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}%`);
});

Следующие шаги
В этом руководстве мы рассмотрели наиболее общий случай взаимодействия между рабочими страницами и сервисами (двунаправленное взаимодействие).
Зачастую для взаимодействия одного пользователя с другим может быть достаточно одного контекста, без получения ответа. Ознакомьтесь со следующими руководствами, чтобы узнать, как реализовать на своих страницах однонаправленные методы взаимодействия между сервис-воркером и сервис-воркером, а также ознакомиться с примерами использования и производственной практики:
- Руководство по императивному кэшированию : вызов сервисного работника со страницы для предварительного кэширования ресурсов (например, в сценариях предварительной выборки).
- Трансляция обновлений : вызов страницы из Service Worker для информирования о важных обновлениях (например, о доступности новой версии веб-приложения).