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

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

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.

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ę po jednej stronie za pomocą interfejsu postMessage() i jest odbierana po drugiej stronie przez implementację 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:

  • 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.
Diagram pokazujący dwukierunkową komunikację między stroną a usługą w tle i 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(), 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.

Diagram przedstawiający dwukierunkową komunikację między stroną a usługą workera za pomocą 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ą.

Client API

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

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

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

Diagram przedstawiający stronę przekazującą port do usługi internetowej, aby nawiązać dwukierunkową komunikację.

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ę 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}%`);
});
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 usługom w tle operacja może być kontynuowana po zamknięciu wszystkich kart (po prawej stronie).

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