В некоторых случаях веб-приложению может потребоваться установить двусторонний канал связи между страницей и сервис-воркером.
Например, в 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.
Использование API браузера
Если библиотеки Workbox недостаточно для ваших нужд, вы можете использовать несколько низкоуровневых API, чтобы реализовать двусторонний обмен данными между страницами и сервис-воркерами. У них есть некоторые сходства и различия:
Сходства:
- Во всех случаях обмен данными начинается на одном конце через интерфейс
postMessage()и принимается на другом конце путем реализации обработчикаmessage. - На практике все доступные API позволяют реализовать одни и те же сценарии использования, но некоторые из них могут упростить разработку в определенных сценариях.
Различия:
- Они по-разному идентифицируют другую сторону связи: некоторые из них используют явную ссылку на другой контекст, в то время как другие могут общаться неявно через прокси-объект, созданный на каждой стороне.
- Поддержка браузеров в них различается.
Broadcast Channel API
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...
}
};
Как видите, явной ссылки на определенный контекст нет, поэтому не нужно сначала получать ссылку на сервис-воркер или определенный клиент.
Недостаток заключается в том, что на момент написания этой статьи API поддерживается в Chrome, Firefox и Edge, но в других браузерах, например Safari, поддержка пока не реализована.
Client API
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 на сайте проверьте, поддерживается ли он браузером.
Канал для сообщений
Для 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 для обработки определенных сценариев: отсутствие подключения и длительные загрузки.
Фоновая синхронизация
Например, в чат-приложении важно, чтобы сообщения не терялись из-за плохого подключения. 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-уведомления из веб-приложения:
Фоновое извлечение
Для относительно коротких задач, таких как отправка сообщения или списка 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}%`);
});
Дальнейшие действия
В этом руководстве мы рассмотрели наиболее общий случай взаимодействия между страницей и сервис-воркерами (двунаправленное взаимодействие).
Часто бывает так, что человеку нужно только передать информацию, не получая ответа. Ознакомьтесь со следующими руководствами, чтобы узнать, как реализовать однонаправленные методы на своих страницах от сервисного работника и к нему, а также варианты использования и примеры производства:
- Руководство по императивному кешированию. Вызов сервисного скрипта со страницы для предварительного кеширования ресурсов (например, при предварительной выборке).
- Рассылка обновлений. Вызов страницы из сервисного скрипта, чтобы сообщить о важных обновлениях (например, о доступности новой версии веб-приложения).