Przewodnik na temat imperatywnego buforowania

Andrew Guan
Andrew Guan

Niektóre witryny mogą potrzebować komunikacji z serwerem roboczym bez konieczności informowania o wyniku. Oto przykłady:

  • Strona wysyła do skryptu service workera listę adresów URL do wstępnego pobierania, aby po kliknięciu linku przez użytkownika dokumenty lub podresury strony były już dostępne w pamięci podręcznej, co znacznie przyspiesza nawigację.
  • Strona prosi skrypt service worker o pobieranie i przechowywanie w pamięci podręcznej zestawu najpopularniejszych artykułów, aby były dostępne w trybie offline.

Delegowanie tego typu zadań niekrytycznych do workera usługi ma tę zaletę, że pozwala zwolnić główny wątek, aby lepiej obsługiwać pilniejsze zadania, takie jak reagowanie na interakcje użytkownika.

Schemat strony proszącej o zasoby do umieszczenia w pamięci podręcznej skryptu service worker.

W tym przewodniku omówimy implementację jednokierunkowej techniki komunikacji z poziomu strony do serwisu workera przy użyciu standardowych interfejsów API przeglądarki i biblioteki Workbox. Takie przypadki użycia nazywamy koniecznym buforowaniem.

Przypadek związany z wersją produkcyjną

Firma 1-800-Flowers.com wdrożyła wymuszone buforowanie (wstępne pobieranie) za pomocą usług działających w tle za pomocą postMessage(), aby wstępnie pobierać najpopularniejsze produkty na stronach kategorii i przyspieszyć nawigację do stron ze szczegółami produktów.

Logo 1-800 Flowers

Aby określić, które elementy mają być pobierane z poziomu pamięci podręcznej, używają podejścia mieszanego:

  • Podczas wczytywania strony proszą usługę o pobieranie danych JSON dotyczących 9 najpopularniejszych elementów i dodawanie uzyskanych obiektów odpowiedzi do pamięci podręcznej.
  • W przypadku pozostałych elementów są one rejestrowane w zdarzeniu mouseover , dzięki czemu gdy użytkownik przesunie kursor na element, może wywołać pobranie zasobu na „żądanie”.

Do przechowywania odpowiedzi w formacie JSON używają interfejsu Cache API:

Logo 1-800 Flowers
Pobieranie wstępnie danych o produktach w formacie JSON z stron z informacjami o produktach na stronie 1-800Flowers.com.

Gdy użytkownik kliknie element, powiązane z nim dane w formacie JSON zostaną pobrane z pamięci podręcznej, co pozwoli na szybszą nawigację bez konieczności korzystania z sieci.

Korzystanie z Workbox

Workbox to łatwy sposób wysyłania wiadomości do service workera za pomocą pakietu workbox-window, czyli zestawu modułów przeznaczonych do uruchamiania w kontekście okna. Uzupełniają one inne pakiety Workbox, które działają w usługach.

Aby umożliwić stronie komunikację ze skryptem service worker, najpierw pobierz odwołanie do obiektu Workbox do zarejestrowanego skryptu service worker:

const wb = new Workbox('/sw.js');
wb.register();

Następnie możesz wysłać wiadomość bezpośrednio w sposób deklaratywny, bez konieczności rejestracji, sprawdzania aktywacji czy zastanawiania się nad interfejsem API do komunikacji:

wb.messageSW({"type": "PREFETCH", "payload": {"urls": ["/data1.json", "data2.json"]}}); });

Usługa w ramach workera implementuje element obsługi message, który będzie nasłuchiwać tych wiadomości. Może on opcjonalnie zwracać odpowiedź, ale w takich przypadkach nie jest to konieczne:

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PREFETCH') {
    // do something
  }
});

Korzystanie z interfejsów API przeglądarek

Jeśli biblioteka Workbox nie spełnia Twoich potrzeb, oto jak za pomocą interfejsów API przeglądarki możesz zaimplementować okna komunikacyjne dla pracowników.

Interfejsu postMessage API można używać do ustanowienia jednokierunkowego mechanizmu komunikacji ze strony do usługi.

Strona wywołuje postMessage() w interfejsie skryptu service worker:

navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
  payload: 'some data to perform the task',
});

Usługa workera implementuje uchwyt message, aby nasłuchiwać tych wiadomości.

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === MSG_ID) {
    // do something
  }
});

Atrybut {type : 'MSG_ID'} nie jest absolutnie wymagany, ale jest jednym ze sposobów na umożliwienie stronie wysyłania różnych typów instrukcji do skryptu service worker (czyli „do wstępnego pobierania” lub „do czyszczenia pamięci”). Na podstawie tego parametru usługa może się rozdzielać na różne ścieżki wykonania.

Jeśli operacja się powiedzie, użytkownik będzie mógł skorzystać z jej zalet, ale jeśli nie, nie zmieni to głównego przebiegu działania. Jeśli na przykład usługa 1-800-Flowers.com próbuje przechwycić dane, strona nie musi wiedzieć, czy udało się to service workerowi. Jeśli tak, użytkownik będzie mógł szybciej się poruszać. Jeśli nie, strona musi przejść na nową stronę. To tylko trochę potrwa.

Prosty przykład wstępnego pobierania

Jednym z najczęstszych zastosowań przymusowego buforowania jest pobieranie zasobów z danego adresu URL z wyprzedzeniem, czyli pobieranie zasobów z danego adresu URL przed tym, jak użytkownik na niego wejdzie, aby przyspieszyć nawigację.

W witrynach można stosować różne metody wstępnego pobierania:

W przypadku stosunkowo prostych scenariuszy wstępnego pobierania, takich jak wstępne pobieranie dokumentów lub konkretnych zasobów (JS, CSS itp.), te techniki są najlepszym rozwiązaniem.

Jeśli wymagana jest dodatkowa logika, np. parsowanie zasobu wstępnego (pliku JSON lub strony) w celu pobrania jego wewnętrznych adresów URL, lepiej jest przekazać to zadanie całkowicie do wykonania procesowi usługi.

Delegowanie tego typu operacji do usługi internetowej ma następujące zalety:

  • Przeniesienie na pomocniczy wątek ciężkiego zadania, jakim jest pobieranie i przetwarzanie pobieranych danych (omówimy to później). Dzięki temu główny wątek może zajmować się ważnymi zadaniami, takimi jak reagowanie na interakcje z użytkownikiem.
  • Umożliwianie wielu klientom (np. zakładkom) ponownego używania wspólnej funkcji, a nawet wywoływania usługi jednocześnie bez blokowania wątku głównego.

Pobieranie wstępnie stron ze szczegółami produktu

Najpierw użyj interfejsu service worker postMessage(), aby przekazać tablicę adresów URL do pamięci podręcznej:

navigator.serviceWorker.controller.postMessage({
  type: 'PREFETCH',
  payload: {
    urls: [
      'www.exmaple.com/apis/data_1.json',
      'www.exmaple.com/apis/data_2.json',
    ],
  },
});

W usługach workera zaimplementuj uchwytywanie message i przetwarzanie wiadomości wysyłanych przez dowolną aktywną kartę:

addEventListener('message', (event) => {
  let data = event.data;
  if (data && data.type === 'PREFETCH') {
    let urls = data.payload.urls;
    for (let i in urls) {
      fetchAsync(urls[i]);
    }
  }
});

W poprzednim kodzie wprowadziliśmy małą funkcję pomocniczą o nazwie fetchAsync(), która iteruje tablicę adresów URL i wysyła żądanie pobierania dla każdego z nich:

async function fetchAsync(url) {
  // await response of fetch call
  let prefetched = await fetch(url);
  // (optionally) cache resources in the service worker storage
}

Po uzyskaniu odpowiedzi możesz polegać na nagłówkach pamięci podręcznej zasobu. W wielu przypadkach jednak, na przykład na stronach z informacjami o produkcie, zasoby nie są przechowywane w pamięci podręcznej (co oznacza, że mają nagłówek Cache-control o wartości no-cache). W takich przypadkach możesz zmienić to zachowanie, przechowując pobrane zasoby w pamięci podręcznej usługi. Dodatkową zaletą jest to, że plik może być wyświetlany w sytuacjach offline.

Dane inne niż JSON

Gdy dane w formacie JSON zostaną pobrane z punktu końcowego serwera, często zawierają one inne adresy URL, które również warto pobrać w ramach wstępnego pobierania, np. obraz lub inne dane punktu końcowego powiązane z tymi danymi pierwszego poziomu.

Załóżmy, że w naszym przykładzie zwrócone dane JSON to informacje o witrynie z produktami spożywczymi:

{
  "productName": "banana",
  "productPic": "https://cdn.example.com/product_images/banana.jpeg",
  "unitPrice": "1.99"
 }

Zmodyfikuj kod fetchAsync(), aby przejść przez listę produktów i zapisać w pamięci podręcznej obraz bohatera dla każdego z nich:

async function fetchAsync(url, postProcess) {
  // await response of fetch call
  let prefetched = await fetch(url);

  //(optionally) cache resource in the service worker cache

  // carry out the post fetch process if supplied
  if (postProcess) {
    await postProcess(prefetched);
  }
}

async function postProcess(prefetched) {
  let productJson = await prefetched.json();
  if (productJson && productJson.product_pic) {
    fetchAsync(productJson.product_pic);
  }
}

Możesz dodać do tego kodu obsługę wyjątków na potrzeby sytuacji takich jak błąd 404. Jednak zaletą używania skryptu service worker do pobierania w poprzedniości jest to, że może on się nie powieść bez większych konsekwencji dla strony i wątku głównego. Możesz też zastosować bardziej złożoną logikę do post-processingu wstępnie pobranych treści, co zwiększy ich elastyczność i uwolni je od zależności od danych, z których korzystają. Nie ma żadnych ograniczeń.

Podsumowanie

W tym artykule omówiliśmy typowy przypadek użycia komunikacji jednokierunkowej między stroną a obsługą klienta: konieczne buforowanie. Omówione przykłady mają tylko na celu pokazanie jednego sposobu korzystania z tego wzorca. Tego samego podejścia można używać w innych przypadkach użycia, na przykład do buforowania najpopularniejszych artykułów na potrzeby offline, dodawania zakładek itp.

Więcej wzorów komunikacji między stroną a usługą:

  • Wyświetlanie powiadomień: wywołanie strony z użyciem service workera w celu poinformowania o ważnych aktualizacjach (np. o dostępnej nowej wersji aplikacji internetowej).
  • Komunikacja dwukierunkowa: delegowanie zadania do service workera (np. pobieranie dużych plików) i informowanie strony o postępach.