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 za pomocą standardowych interfejsów API przeglądarki i biblioteki Workbox. Takie przypadki użycia nazywamy koniecznym buforowaniem.

Przypadek środowisk produkcyjnych

Firma 1-800-Flowers.com wdrożyła przymusowe buforowanie (wstępne pobieranie) za pomocą usług działających w tle za pomocą postMessage(), aby pobierać wstępnie 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 wywoływane w związku ze zdarzeniem 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 na wysyłanie wiadomości do workera usługi 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 bezpośrednio wysłać wiadomość 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 komunikacji z usługą.

Interfejsu postMessage API można używać do ustanowienia jednokierunkowego mechanizmu komunikacji ze strony do workera 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 w ramach workera implementuje element obsługi message, który będzie 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 opróżniania 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ł z niej korzystać. W przeciwnym razie 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 pracownikowi usługi. Jeśli tak, użytkownik będzie mógł szybciej się poruszać. Jeśli tak nie jest, strona musi przejść do nowej strony. To tylko trochę potrwa.

Prosty przykład wstępnego pobierania

Jednym z najczęstszych zastosowań obowiązkowego buforowania jest pobieranie zasobów z zapleczem, czyli pobieranie zasobów dla danego adresu URL przed tym, jak użytkownik go otworzy, 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.

Wstępne pobieranie stron ze szczegółami produktów

Najpierw użyj interfejsu service workera 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ługowym workerze zaimplementuj handler message, aby przechwytywać i przetwarzać wiadomości wysyłane 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ą umieszczane 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

Po pobraniu danych JSON 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 zwracane dane JSON to informacje o witrynie sklepu spożywczego:

{
  "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 takich sytuacji jak błąd 404. Jednak zaletą korzystania ze 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, np. 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 workera usługowego w celu poinformowania o ważnych aktualizacjach (np. o dostępnej nowej wersji aplikacji internetowej).
  • Komunikacja dwukierunkowa: delegowanie zadania do usługi internetowej (np. pobieranie dużych plików) i informowanie strony o postępach.