Imperativer Leitfaden zum Caching

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

Einige Websites müssen möglicherweise mit dem Service Worker kommunizieren, ohne über das Ergebnis informiert zu werden. Hier einige Beispiele:

  • Eine Seite sendet dem Service Worker eine Liste mit URLs zum Prefetchen. Wenn der Nutzer dann auf einen Link klickt, sind die Unterressourcen des Dokuments oder der Seitenunterressourcen bereits im Cache verfügbar. Dadurch wird die nachfolgende Navigation viel schneller.
  • Die Seite fordert den Service Worker auf, eine Reihe von Top-Artikeln abzurufen und im Cache zu speichern, damit sie offline verfügbar sind.

Das Delegieren dieser Arten von nicht kritischen Aufgaben an den Service Worker hat den Vorteil, dass der Hauptthread für dringlichere Aufgaben wie das Reagieren auf Nutzerinteraktionen freigegeben wird.

Diagramm einer Seite, die Ressourcen zum Zwischenspeichern bei einem Service Worker anfordert.

In diesem Leitfaden erfahren Sie, wie Sie mithilfe von Standard-Browser-APIs und der Workbox-Bibliothek eine Einweg-Kommunikationstechnik von der Seite an den Service Worker implementieren. Diese Arten von Anwendungsfällen bezeichnen wir als imperatives Caching.

Produktionsfall

1-800-Flowers.com implementierte imperatives Caching (Prefetching) bei Service Workern über postMessage(), um die Top-Elemente auf Kategorieseiten vorab abzurufen und die nachfolgende Navigation zu Produktdetailseiten zu beschleunigen.

Logo von 1-800 Flowers.

Sie verwenden einen gemischten Ansatz, um zu entscheiden, welche Elemente vorab abgerufen werden sollen:

  • Beim Seitenaufbau wird der Servicer-Worker aufgefordert, die JSON-Daten für die Top-9-Elemente abzurufen und die resultierenden Antwortobjekte dem Cache hinzuzufügen.
  • Für die verbleibenden Elemente wird das Ereignis mouseover überwacht. Wenn ein Nutzer also den Cursor auf ein Element bewegt, kann er bei Bedarf einen Abruf der Ressource auslösen.

Sie verwenden die Cache API zum Speichern von JSON-Antworten:

Logo von 1-800 Flowers.
JSON-Produktdaten werden vorab von Seiten mit Produkteinträgen in 1-800Flowers.com abgerufen.

Wenn der Nutzer auf ein Element klickt, können die zugehörigen JSON-Daten aus dem Cache abgerufen werden, ohne dass eine Netzwerkverbindung hergestellt werden muss. Dadurch wird die Navigation beschleunigt.

Workbox verwenden

Workbox bietet eine einfache Möglichkeit, Nachrichten an einen Service Worker zu senden. Verwenden Sie dazu das Paket workbox-window, eine Reihe von Modulen, die im Fensterkontext ausgeführt werden sollen. Sie sind eine Ergänzung zu den anderen Workbox-Paketen, die im Service Worker ausgeführt werden.

Um die Seite mit dem Service Worker zu kommunizieren, rufen Sie zuerst einen Workbox-Objektverweis auf den registrierten Service Worker ab:

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

Anschließend können Sie die Nachricht direkt deklarativ senden, ohne sich um die Registrierung, die Überprüfung auf Aktivierung oder die zugrunde liegende Kommunikations-API kümmern zu müssen:

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

Der Service Worker implementiert einen message-Handler, um diese Nachrichten zu überwachen. Optional kann eine Antwort zurückgegeben werden. In solchen Fällen ist dies jedoch nicht erforderlich:

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

Browser-APIs verwenden

Wenn die Workbox-Bibliothek für Ihre Anforderungen nicht ausreicht, können Sie die Window-to-Service-Worker-Kommunikation mithilfe von Browser-APIs wie folgt implementieren.

Mit der postMessage API kann ein einseitiger Kommunikationsmechanismus von der Seite zum Service Worker eingerichtet werden.

Die Seite ruft postMessage() auf der Service Worker-Oberfläche auf:

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

Der Service Worker implementiert einen message-Handler, um diese Nachrichten zu überwachen.

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

Das Attribut {type : 'MSG_ID'} ist nicht unbedingt erforderlich, aber es ist eine Möglichkeit, der Seite zu ermöglichen, verschiedene Arten von Anweisungen an den Service Worker zu senden (d. h. zum Vorabrufen statt zum Löschen des Speichers). Der Service Worker kann sich basierend auf diesem Flag in verschiedene Ausführungspfade verzweigen.

Wenn der Vorgang erfolgreich war, konnten die Nutzenden von den Vorteilen profitieren. Andernfalls wird der grundlegende User Flow nicht verändert. Wenn beispielsweise 1-800-Flowers.com versucht, einen Vorab-Cache zu speichern, muss die Seite nicht wissen, ob der Service Worker erfolgreich war. Wenn dies der Fall ist, profitieren die Nutzenden von einer schnelleren Navigation. Wenn dies nicht der Fall ist, muss die Seite noch zur neuen Seite wechseln. Es dauert nur etwas länger.

Ein einfaches Beispiel für einen Vorabruf

Eine der häufigsten Anwendungen des imperativen Caching ist der Vorabruf. Dies bedeutet, dass Ressourcen für eine URL abgerufen werden, bevor der Nutzer zu ihr wechselt, um die Navigation zu beschleunigen.

Es gibt verschiedene Möglichkeiten, den Vorabruf in Websites zu implementieren:

  • Link-Prefetch-Tags auf Seiten verwenden: Ressourcen werden fünf Minuten lang im Browsercache gespeichert. Danach gelten die normalen Cache-Control-Regeln für die Ressource.
  • Ergänzung der vorherigen Technik durch eine Laufzeit-Caching-Strategie im Service Worker, um die Lebensdauer der Prefetch-Ressource über dieses Limit hinaus zu verlängern.

Bei relativ einfachen Prefetch-Szenarien wie dem Vorabruf von Dokumenten oder bestimmten Assets (JS, CSS usw.) sind diese Techniken der beste Ansatz.

Wenn zusätzliche Logik erforderlich ist, z. B. das Parsen der Prefetch-Ressource (eine JSON-Datei oder -Seite), um die internen URLs abzurufen, empfiehlt es sich, diese Aufgabe vollständig an den Service Worker zu delegieren.

Das Delegieren dieser Arten von Vorgängen an den Service Worker bietet folgende Vorteile:

  • Die mühsame Abruf- und Nachabrufverarbeitung (die später eingeführt wird) wird auf einen sekundären Thread verlagert. Dadurch wird der Hauptthread für wichtigere Aufgaben wie das Antworten auf Nutzerinteraktionen freigegeben.
  • Mehrere Clients (z.B. Tabs) können eine gemeinsame Funktionalität wiederverwenden und den Dienst sogar gleichzeitig aufrufen, ohne den Hauptthread zu blockieren.

Produktdetailseiten vorabrufen

Verwenden Sie zuerst postMessage() auf der Service Worker-Schnittstelle und übergeben Sie ein Array mit URLs, die im Cache gespeichert werden sollen:

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

Implementieren Sie im Service Worker einen message-Handler, um Nachrichten abzufangen und zu verarbeiten, die von einem aktiven Tab gesendet werden:

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

Im vorherigen Code haben wir eine kleine Hilfsfunktion namens fetchAsync() eingeführt, um das URL-Array zu iterieren und für jede URL eine Abrufanfrage zu senden:

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

Wenn die Antwort eingeht, können Sie sich auf die Caching-Header der Ressource verlassen. In vielen Fällen werden Ressourcen, z. B. auf Produktdetailseiten, nicht im Cache gespeichert. Sie haben also den Cache-control-Header no-cache. In solchen Fällen können Sie dieses Verhalten überschreiben, indem Sie die abgerufene Ressource im Service Worker-Cache speichern. Dies hat den zusätzlichen Vorteil, dass die Datei auch offline bereitgestellt werden kann.

Über JSON-Daten hinaus

Sobald die JSON-Daten von einem Serverendpunkt abgerufen wurden, enthalten sie oft andere URLs, die ebenfalls einen Vorabruf wert sind, z. B. ein Bild oder andere Endpunktdaten, die mit diesen Daten der ersten Ebene verknüpft sind.

Nehmen wir an, dass in unserem Beispiel die zurückgegebenen JSON-Daten die Informationen einer Website für Lebensmittel sind:

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

Ändern Sie den fetchAsync()-Code, um die Liste der Produkte zu iterieren, und speichern Sie das Hero-Image für jedes Produkt im Cache:

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

Sie können um diesen Code herum eine Ausnahmebehandlung für Situationen wie 404-Fehler hinzufügen. Das Schöne an der Verwendung eines Service Workers für den Prefetch ist jedoch, dass er ohne große Auswirkungen auf die Seite und den Hauptthread fehlschlagen kann. Möglicherweise verwenden Sie auch eine ausgefeiltere Logik für die Nachbearbeitung des Prefetch-Inhalts, sodass dieser flexibler und von den verarbeiteten Daten entkoppelt wird. Alles ist möglich.

Fazit

In diesem Artikel wurde ein häufiger Anwendungsfall der einseitigen Kommunikation zwischen Seiten und Service Worker behandelt: imperatives Caching. Die erläuterten Beispiele dienen nur dazu, eine Möglichkeit zu zeigen, wie dieses Muster verwendet werden kann. Der gleiche Ansatz kann auch auf andere Anwendungsfälle angewendet werden, z. B. das Speichern von Top-Artikeln on demand für die Offlinenutzung, das Speichern von Lesezeichen usw.

Weitere Muster der Seiten- und Service-Worker-Kommunikation finden Sie hier:

  • Broadcast-Updates: Durch Aufrufen der Seite durch den Service Worker werden Sie über wichtige Updates informiert (z.B. wenn eine neue Version der Web-App verfügbar ist).
  • Zwei-Wege-Kommunikation: Delegieren einer Aufgabe an einen Service Worker, z.B. bei einem umfangreichen Download, und Informieren der Seite über den Fortschritt.