Dwukierunkowa komunikacja z skryptami service worker

Andrew Guan
Andrew Guan

W niektórych przypadkach aplikacja internetowa może potrzebować dwukierunkowego kanału komunikacji między stroną a usługą.

Na przykład w przypadku podcastów PWA można utworzyć funkcję umożliwiającą użytkownikowi pobieranie odcinków do odtwarzania offline oraz przekazywanie usłudze service worker regularnie informowania strony o postępach, tak aby wątek główny mógł aktualizować interfejs.

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.

Diagram pokazujący skrypt service worker i stronę wymieniające się wiadomościami.

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);

Skrypt service worker implementuje detektor wiadomości i odpowiada zarejestrowanemu skryptowi:

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.

Diagram przedstawiający dwukierunkową komunikację między stroną a usługą workera za pomocą okna Workbox

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ę z jednej strony przez interfejs postMessage(), a jest odbierana z drugiej przez wdrożenie modułu obsługi message.
  • 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:

  • Druga strona komunikacji jest w nich identyfikowana na różne sposoby: niektóre z nich korzystają w sposób jednoznaczny z drugim kontekstem, a inne mogą porozumiewać się w sposób niejawny za pomocą instancji obiektu serwera proxy utworzonego po każdej stronie.
  • Różne przeglądarki obsługują różne przeglądarki.
Diagram przedstawiający dwukierunkową komunikację między stroną a usługą w tle oraz dostępne interfejsy API przeglądarki

Interfejs Broadcast Channel API

Obsługa przeglądarek

  • Chrome: 54.
  • Edge: 79.
  • Firefox: 38.
  • Safari: 15.4.

Źródło

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() do wysyłania 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.

Diagram przedstawiający dwukierunkową komunikację między stroną a usługą workera przy użyciu obiektu BroadcastChannel

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ą.

Interfejs API klienta

Obsługa przeglądarek

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1

Źródło

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()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'});
  }
});
Diagram przedstawiający pracownika usługi komunikującego się z wielu klientami

Client API to dobry sposób na stosunkowo proste komunikowanie się ze wszystkimi aktywnymi kartami skryptu service worker. 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

Obsługa przeglądarek

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41.
  • Safari: 5.

Źródło

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 używa go do wysłania portu do zarejestrowanego mechanizmu Service Worker. Strona ma też zaimplementowany detektor onmessage, który odbiera wiadomości z drugiego 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
};
Diagram przedstawiający stronę przekazującą port do usługi internetowej, aby nawiązać dwukierunkową komunikację.

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 stosunkowo prostych przypadkach, takich jak przekazywanie komunikatu w formie ciągu znaków opisującym wykonywaną operację lub listę adresów URL, które mają być buforowane z jednego kontekstu w drugim. W tej sekcji omówimy 2 interfejsy API do obsługi określonych scenariuszy: braku połączenia i długiego pobierania.

Synchronizacja w tle

Obsługa przeglądarek

  • Chrome: 49.
  • Edge: 79.
  • Firefox: nieobsługiwane.
  • Safari: nieobsługiwane.

Źródło

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. Dzięki temu masz pewność, że to, co użytkownik chce wysłać, zostanie rzeczywiście 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 skrypt service worker może następnie komunikować się z powrotem ze stroną, aby zaktualizować interfejs użytkownika przy użyciu dowolnego z przedstawionych 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:

Diagram przedstawiający stronę przekazującą port do skryptu service worker w celu nawiązania komunikacji dwukierunkowej.

Pobranie w tle

Obsługa przeglądarek

  • Chrome: 74.
  • Edge: 79.
  • Firefox: nieobsługiwane.
  • Safari: nieobsługiwane.

Źródło

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ę z skryptem service worker z poziomu strony, używaj backgroundFetch.fetch zamiast 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,
    },
  );
});

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}%`);
});
Diagram przedstawiający stronę przekazującą port do usługi internetowej, aby nawiązać dwukierunkową komunikację.
Interfejs jest aktualizowany, aby wskazywać postęp pobierania (po lewej). Dzięki skryptom Service Worker operacja może być kontynuowana, gdy wszystkie karty zostaną zamknięte (po prawej).

Dalsze kroki

W tym przewodniku omówiliśmy najbardziej ogólny przypadek komunikacji między stroną a usługami (komunikacja dwukierunkowa).

Często do komunikowania się z drugim potrzebnym jest tylko jeden kontekst, nie otrzymując 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 imperatywnym buforowaniu: wywoływanie skryptu service worker ze strony w celu z wyprzedzeniem buforowania zasobów (np. w scenariuszach wstępnego wczytywania).
  • Rozgłaszanie aktualizacji: wywołanie strony z użyciem service workera w celu poinformowania o ważnych aktualizacjach (np. o dostępnej nowej wersji aplikacji internetowej).