W niektórych przypadkach aplikacja internetowa może potrzebować dwukierunkowego kanału komunikacji między stroną a workerem usługi.
Na przykład w PWA z podcastami można utworzyć funkcję, która umożliwia użytkownikowi pobieranie odcinków na potrzeby odsłuchiwania offline, oraz umożliwić workerowi usługi regularne informowanie strony o postępach, aby główny wątek mógł aktualizować interfejs użytkownika.
W tym przewodniku omówimy różne sposoby implementowania dwukierunkowej komunikacji między kontekstem okna a obsługą usługi, korzystając z różnych interfejsów API, biblioteki Workbox oraz niektórych zaawansowanych przypadków.
Korzystanie z Workbox
workbox-window
to zestaw modułów biblioteki Workbox, które mają działać w kontekście okna. Klasa Workbox
udostępnia metodę messageSW()
, która umożliwia wysłanie wiadomości do zarejestrowanego w instancji pracownika usługi i odczekanie na odpowiedź.
Poniższy kod strony tworzy nową instancję Workbox
i wysyła wiadomość do usługi, aby pobrać jej wersję:
const wb = new Workbox('/sw.js');
wb.register();
const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);
Usługa workera implementuje odsłuchiwanie wiadomości po drugiej stronie i reaguje na zarejestrowaną usługę workera:
const SW_VERSION = '1.0.0';
self.addEventListener('message', (event) => {
if (event.data.type === 'GET_VERSION') {
event.ports[0].postMessage(SW_VERSION);
}
});
Pod maską biblioteka korzysta z interfejsu API przeglądarki, który omówimy w następnej sekcji: MessageChannel, ale abstrahuje wiele szczegółów implementacji, co ułatwia jej używanie, a zarazem wykorzystuje obsługę wielu przeglądarek, którą zapewnia ten interfejs API.
Korzystanie z interfejsów API przeglądarek
Jeśli biblioteka Workbox nie spełnia Twoich potrzeb, możesz skorzystać z kilku interfejsów API niższego poziomu, aby zaimplementować „dwukierunkową” komunikację między stronami a workerami usługi. Wykazują pewne podobieństwa i różnice:
Podobieństwa:
- We wszystkich przypadkach komunikacja rozpoczyna się po jednej stronie za pomocą interfejsu
postMessage()
i jest odbierana po drugiej stronie przez implementację modułu obsługimessage
. - W praktyce wszystkie dostępne interfejsy API umożliwiają implementację tych samych przypadków użycia, ale niektóre z nich mogą uprościć proces tworzenia w niektórych scenariuszach.
Różnice:
- Mają różne sposoby identyfikowania drugiej strony komunikacji: niektóre z nich używają jawnego odwołania do innego kontekstu, a inne mogą komunikować się pośrednio za pomocą obiektu pośredniczącego utworzonego po obu stronach.
- Obsługa przeglądarek różni się w zależności od przeglądarki.
Interfejs Broadcast Channel API
Interfejs Broadcast Channel API umożliwia podstawową komunikację między kontekstami przeglądania za pomocą obiektów BroadcastChannel.
Aby to zaimplementować, każdy kontekst musi najpierw utworzyć instancję obiektu BroadcastChannel
o tym samym identyfikatorze i wysyłać oraz odbierać z niego wiadomości:
const broadcast = new BroadcastChannel('channel-123');
Obiekt BroadcastChannel udostępnia interfejs postMessage()
, który umożliwia wysyłanie wiadomości do dowolnego kontekstu nasłuchującego:
//send message
broadcast.postMessage({ type: 'MSG_ID', });
Każdy kontekst przeglądarki może odbierać wiadomości za pomocą metody onmessage
obiektu BroadcastChannel
:
//listen to messages
broadcast.onmessage = (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//process message...
}
};
Jak widać, nie ma wyraźnego odwołania do konkretnego kontekstu, więc nie trzeba najpierw uzyskać odwołania do service workera ani żadnego konkretnego klienta.
W chwili pisania tego artykułu interfejs API jest obsługiwany przez przeglądarki Chrome, Firefox i Edge, ale inne przeglądarki, takie jak Safari, jeszcze go nie obsługują.
Client API
Interfejs Client API umożliwia uzyskanie odwołania do wszystkich obiektów WindowClient
reprezentujących aktywne karty, które kontroluje usługa działająca w tle.
Strona jest kontrolowana przez pojedynczy skrypt service worker, który nasłuchuje i wysyła wiadomości do aktywnego skryptu service worker bezpośrednio za pomocą interfejsu 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
}
};
Podobnie usługa nasłuchuje wiadomości, implementując listenera onmessage
:
//listen to messages
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//Process message
}
});
Aby komunikować się z dowolnym klientem, pracownik usługi uzyskuje tablicę obiektów WindowClient
, wykonując metody takie jak Clients.matchAll()
i Clients.get()
. Następnie może
postMessage()
wykonać jedną z tych czynności:
//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
to dobra opcja, która umożliwia łatwe komunikowanie się ze wszystkimi aktywnymi kartami z poziomu usługi w łatwy sposób. Interfejs API jest obsługiwany przez wszystkie główne przeglądarki, ale nie wszystkie jego metody mogą być dostępne. Przed wdrożeniem interfejsu w witrynie sprawdź, czy jest on obsługiwany przez przeglądarkę.
Kanał wiadomości
Message Channel wymaga zdefiniowania i przekazania portu z jednego kontekstu do drugiego w celu utworzenia dwukierunkowego kanału komunikacji.
Aby zainicjować kanał, strona tworzy instancję obiektu MessageChannel
i wykorzystuje go do wysłania portu do zarejestrowanego pracownika usługi. Strona implementuje też listenera onmessage
, aby odbierać wiadomości z innego kontekstu:
const messageChannel = new MessageChannel();
//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
messageChannel.port2,
]);
//Listen to messages
messageChannel.port1.onmessage = (event) => {
// Process message
};
Usługa otrzymuje port, zapisuje odwołanie do niego i wykorzystuje go do wysłania wiadomości do drugiej strony:
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
jest obecnie obsługiwana przez wszystkie główne przeglądarki.
Zaawansowane interfejsy API: synchronizacja w tle i pobieranie w tle
W tym przewodniku omówiliśmy sposoby wdrażania technik komunikacji dwukierunkowej w przypadku stosunkowo prostych przypadków, takich jak przekazywanie ciągu znaków opisującego operację do wykonania lub listy adresów URL do pamięci podręcznej z jednego kontekstu do drugiego. W tej sekcji omówimy 2 interfejsy API do obsługi określonych scenariuszy: braku połączenia i długiego pobierania.
Synchronizacja w tle
Aplikacja do czatu może chcieć mieć pewność, że wiadomości nigdy nie zostaną utracone z powodu słabego połączenia. Interfejs Background Sync API umożliwia odroczenie działań, które zostaną powtórzone, gdy użytkownik będzie mieć stabilne połączenie. Jest to przydatne, aby mieć pewność, że wszystko, co użytkownik chce wysłać, zostanie wysłane.
Zamiast interfejsu postMessage()
strona rejestruje sync
:
navigator.serviceWorker.ready.then(function (swRegistration) {
return swRegistration.sync.register('myFirstSync');
});
Następnie usługa wątek oczekuje na zdarzenie sync
, aby przetworzyć wiadomość:
self.addEventListener('sync', function (event) {
if (event.tag == 'myFirstSync') {
event.waitUntil(doSomeStuff());
}
});
Funkcja doSomeStuff()
powinna zwracać obietnicę wskazującą, czy udało się wykonać daną czynność. Jeśli tak, synchronizacja została zakończona. Jeśli się nie powiedzie, zostanie zaplanowana kolejna synchronizacja. Ponowna synchronizacja również czeka na połączenie i wykorzystuje wzrastający czas do ponowienia.
Po wykonaniu operacji usługa może się komunikować z stroną, aby zaktualizować interfejs użytkownika, korzystając z dowolnego z wyjaśnionych wcześniej interfejsów API do komunikacji.
Wyszukiwarka Google używa synchronizacji w tle, aby przechowywać nieudane zapytania z powodu słabego połączenia i ponownie je próbować, gdy użytkownik jest online. Po wykonaniu operacji powiadomienie push zostanie wysłane do użytkownika:
Pobranie w tle
W przypadku stosunkowo krótkich zadań, takich jak wysyłanie wiadomości lub lista adresów URL do umieszczenia w pamięci podręcznej, do tej pory zbadane opcje są dobrym wyborem. Jeśli zadanie zajmie zbyt dużo czasu, przeglądarka zakończy działanie service workera. W przeciwnym razie może to stanowić zagrożenie dla prywatności i baterii użytkownika.
Interfejs Background Fetch API umożliwia przeniesienie długiego zadania do workera usługi, np. pobieranie filmów, podcastów lub poziomów gry.
Aby komunikować się ze skryptem service worker ze strony, zamiast postMessage()
użyj backgroundFetch.fetch
:
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,
},
);
});
Obiekt BackgroundFetchRegistration
umożliwia stronie odbieranie zdarzenia progress
, aby śledzić postęp pobierania:
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}%`);
});
Dalsze kroki
W tym przewodniku omówiliśmy najbardziej ogólny przypadek komunikacji między stroną a usługami (komunikacja dwukierunkowa).
Często do komunikacji z innym użytkownikiem wystarczy tylko jeden kontekst, bez otrzymywania odpowiedzi. Aby dowiedzieć się, jak wdrażać techniki jednokierunkowe na stronach, z których korzysta usługa workera, zapoznaj się z tymi przewodnikami, a także z przypadkami użycia i przykładami wdrożenia w produkcji:
- Przewodnik po pamięci podręcznej imperatywnej: wywołanie skryptu service worker ze strony w celu wcześniejszego zapisania zasobów w pamięci podręcznej (np. w scenariuszach wstępnego pobierania).
- Wyświetlanie powiadomień: wywołanie strony z użyciem workera usługowego w celu poinformowania o ważnych aktualizacjach (np. o dostępnej nowej wersji aplikacji internetowej).