Cross-Origin Service Workers – Tests mit Foreign Fetch

Hintergrund

Service Worker ermöglichen es Webentwicklern, auf Netzwerkanfragen ihrer Webanwendungen zu reagieren, sodass sie auch offline weiterarbeiten, Lie-Fi bekämpfen und komplexe Cache-Interaktionen wie Stale-While-Revalidate implementieren können. Service Worker waren jedoch bisher an einen bestimmten Ursprung gebunden. Als Inhaber einer Webanwendung sind Sie dafür verantwortlich, einen Service Worker zu schreiben und bereitzustellen, der alle Netzwerkanfragen Ihrer Webanwendung abfängt. In diesem Modell ist jeder Service Worker für die Verarbeitung von Anfragen zwischen verschiedenen Ursprüngen verantwortlich, z. B. an eine Drittanbieter-API oder für Web-Schriftarten.

Was wäre, wenn ein Drittanbieter einer API, Webfonts oder eines anderen häufig verwendeten Dienstes seinen eigenen Service Worker bereitstellen könnte, der Anfragen von anderen Ursprüngen an seinen Ursprung weiterleiten könnte? Anbieter können ihre eigene benutzerdefinierte Netzwerklogik implementieren und eine einzelne autoritative Cache-Instanz zum Speichern ihrer Antworten nutzen. Dank foreign fetch ist diese Art der Bereitstellung von Drittanbieter-Dienstworkern jetzt Realität.

Die Bereitstellung eines Service Workers, der Foreign Fetch implementiert, ist für jeden Anbieter eines Dienstes sinnvoll, auf den über HTTPS-Anfragen von Browsern zugegriffen wird. Denken Sie nur an Szenarien, in denen Sie eine netzwerkunabhängige Version Ihres Dienstes bereitstellen könnten, in der Browser einen gemeinsamen Ressourcencache nutzen könnten. Beispiele für Dienste, die davon profitieren könnten:

  • API-Anbieter mit RESTful-Schnittstellen
  • Anbieter von Webschriften
  • Anbieter von Analysetools
  • Anbieter von Bild-Hosting
  • Generische Content Delivery Networks

Angenommen, Sie sind ein Analyseanbieter. Wenn Sie einen externen Dienst-Worker für den Abruf bereitstellen, können Sie dafür sorgen, dass alle Anfragen an Ihren Dienst, die fehlschlagen, während ein Nutzer offline ist, in der Warteschlange platziert und wiedergegeben werden, sobald die Verbindung wiederhergestellt ist. Es war zwar schon immer möglich, dass die Clients eines Dienstes ein ähnliches Verhalten über eigene Service Worker implementieren, aber es ist nicht so skalierbar, wenn jeder Client eine maßgeschneiderte Logik für Ihren Dienst schreiben muss, wie wenn Sie einen freigegebenen externen Fetch-Service Worker bereitstellen.

Vorbereitung

Ursprungstest-Token

Die Funktion „Externer Abruf“ befindet sich noch in der Testphase. Um zu vermeiden, dass dieses Design vorzeitig implementiert wird, bevor es vollständig von den Browseranbietern spezifiziert und vereinbart wurde, wurde es in Chrome 54 als Origin Trial implementiert. Solange die externe Abruffunktion experimentell ist, musst du ein Token anfordern, das auf den Ursprung deines Dienstes beschränkt ist, um diese neue Funktion mit dem von dir gehosteten Dienst zu verwenden. Das Token muss als HTTP-Antwortheader in allen plattformübergreifenden Anfragen für Ressourcen enthalten sein, die Sie über Foreign Fetch verarbeiten möchten, sowie in der Antwort für Ihre Service Worker-JavaScript-Ressource:

Origin-Trial: token_obtained_from_signup

Der Testzeitraum endet im März 2017. Bis dahin sollten wir alle Änderungen gefunden haben, die zur Stabilisierung der Funktion erforderlich sind, und sie (hoffentlich) standardmäßig aktivieren. Wenn die Abfrage externer Daten bis dahin nicht standardmäßig aktiviert ist, funktionieren die Funktionen, die mit vorhandenen Origin Trial-Tokens verknüpft sind, nicht mehr.

Wenn Sie vor der Registrierung für ein offizielles Origin Trial-Token mit dem Abrufen von externen Inhalten experimentieren möchten, können Sie die Anforderung in Chrome für Ihren lokalen Computer umgehen. Rufen Sie dazu chrome://flags/#enable-experimental-web-platform-features auf und aktivieren Sie die Option „Experimentelle Webplattform-Funktionen“. Hinweis: Diese Schritte müssen in jeder Chrome-Instanz ausgeführt werden, die Sie in Ihren lokalen Tests verwenden möchten. Mit einem Origin Trial-Token ist die Funktion dagegen für alle Ihre Chrome-Nutzer verfügbar.

HTTPS

Wie bei allen Service Worker-Bereitstellungen muss auf den Webserver, auf dem sowohl Ihre Ressourcen als auch Ihr Service Worker-Script bereitgestellt werden, über HTTPS zugegriffen werden. Außerdem gilt die Abfangung von externen Abrufen nur für Anfragen, die von Seiten stammen, die auf sicheren Ursprüngen gehostet werden. Die Clients Ihres Dienstes müssen also HTTPS verwenden, um die Implementierung von externen Abrufen nutzen zu können.

Foreign Fetch verwenden

Nachdem wir die Voraussetzungen geklärt haben, sehen wir uns die technischen Details an, die erforderlich sind, um einen Worker für einen externen Abrufdienst einzurichten.

Service Worker registrieren

Die erste Herausforderung, auf die Sie wahrscheinlich stoßen werden, ist die Registrierung Ihres Service Workers. Wenn Sie schon einmal mit Service Workern gearbeitet haben, sind Ihnen wahrscheinlich die folgenden Punkte bekannt:

// You can't do this!
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('service-worker.js');
}

Dieser JavaScript-Code für die Registrierung eines eigenen Dienstarbeiters ist im Kontext einer Webanwendung sinnvoll, die ausgelöst wird, wenn ein Nutzer eine von Ihnen verwaltete URL aufruft. Es ist jedoch keine praktikable Lösung, einen Service Worker eines Drittanbieters zu registrieren, wenn der Browser nur eine bestimmte Unterressource und keine vollständige Navigation von Ihrem Server anfordert. Wenn der Browser beispielsweise ein Bild von einem von Ihnen verwalteten CDN-Server anfordert, können Sie dieses JavaScript-Snippet nicht an den Anfang Ihrer Antwort setzen und erwarten, dass es ausgeführt wird. Es ist eine andere Methode zur Registrierung von Dienstarbeitern außerhalb des normalen JavaScript-Ausführungskontexts erforderlich.

Die Lösung besteht aus einem HTTP-Header, den Ihr Server in jede Antwort aufnehmen kann:

Link: </service-worker.js>; rel="serviceworker"; scope="/"

Sehen wir uns diesen Beispielheader einmal genauer an. Die einzelnen Komponenten sind durch das Zeichen ; voneinander getrennt.

  • </service-worker.js> ist erforderlich und wird verwendet, um den Pfad zur Service Worker-Datei anzugeben. Ersetzen Sie /service-worker.js durch den entsprechenden Pfad zu Ihrem Script. Dies entspricht direkt dem String scriptURL, der ansonsten als erster Parameter an navigator.serviceWorker.register() übergeben würde. Der Wert muss in <>-Zeichen eingeschlossen sein (wie in der Link-Header-Spezifikation gefordert). Wenn eine relative statt einer absoluten URL angegeben wird, wird sie als relativ zum Speicherort der Antwort interpretiert.
  • rel="serviceworker" ist ebenfalls erforderlich und sollte ohne Anpassungen enthalten sein.
  • scope=/ ist eine optionale Bereichsdeklaration, die dem String options.scope entspricht, den Sie als zweiten Parameter an navigator.serviceWorker.register() übergeben können. Für viele Anwendungsfälle ist der Standardumfang ausreichend. Sie können diesen Parameter also weglassen, es sei denn, Sie wissen, dass Sie ihn benötigen. Für die Registrierung von Link-Headern gelten dieselben Einschränkungen hinsichtlich des maximal zulässigen Gültigkeitsbereichs sowie die Möglichkeit, diese Einschränkungen über den Header Service-Worker-Allowed zu lockern.

Genau wie bei einer „traditionellen“ Service Worker-Registrierung wird mit dem Link-Header ein Service Worker installiert, der für die nächste Anfrage an den registrierten Umfang verwendet wird. Der Antworttext mit dem speziellen Header wird unverändert verwendet und ist für die Seite sofort verfügbar, ohne dass auf die Fertigstellung der Installation des externen Service Workers gewartet werden muss.

Fremdabrufe werden derzeit als Ursprungstest implementiert. Daher müssen Sie neben dem Link-Antwortheader auch einen gültigen Origin-Trial-Header angeben. Die Mindestanzahl an Antwortheadern, die Sie hinzufügen müssen, um Ihren Worker für den externen Abrufdienst zu registrieren, ist

Link: </service-worker.js>; rel="serviceworker"
Origin-Trial: token_obtained_from_signup

Registrierung debuggen

Während der Entwicklung sollten Sie prüfen, ob der Worker für den externen Abrufdienst richtig installiert ist und Anfragen verarbeitet. In den Entwicklertools von Chrome können Sie einige Dinge prüfen, um sicherzustellen, dass alles wie erwartet funktioniert.

Werden die richtigen Antwortheader gesendet?

Um den externen Fetch-Dienst-Worker zu registrieren, müssen Sie einen Link-Header in einer Antwort auf eine Ressource festlegen, die auf Ihrer Domain gehostet wird, wie bereits weiter oben in diesem Beitrag beschrieben. Während des Ursprungstests müssen Sie, sofern Sie chrome://flags/#enable-experimental-web-platform-features nicht festgelegt haben, auch einen Origin-Trial-Antwortheader festlegen. Ob Ihr Webserver diese Header setzt, können Sie im Bereich „Netzwerk“ der Entwicklertools prüfen:

Header, die im Bereich „Netzwerk“ angezeigt werden.

Ist der Service Worker für den externen Abruf ordnungsgemäß registriert?

Sie können die zugrunde liegende Service Worker-Registrierung einschließlich ihres Umfangs auch in der vollständigen Liste der Service Worker im Bereich „Anwendung“ von DevTools prüfen. Wählen Sie die Option „Alle anzeigen“ aus, da standardmäßig nur Dienstprogramme für den aktuellen Ursprung angezeigt werden.

Der Worker des externen Abrufdiensts im Bereich „Anwendungen“.

Der Installations-Event-Handler

Nachdem Sie den Service Worker des Drittanbieters registriert haben, kann er wie jeder andere Service Worker auf die Ereignisse install und activate reagieren. Diese Ereignisse können beispielsweise genutzt werden, um während des Ereignisses install Caches mit erforderlichen Ressourcen zu füllen oder veraltete Caches im Ereignis activate zu bereinigen.

Zusätzlich zu den normalen install-Ereignis-Caching-Aktivitäten ist im install-Ereignishandler des Drittanbieter-Dienstearbeiters ein zusätzlicher Schritt erforderlich. Ihr Code muss registerForeignFetch() aufrufen, wie im folgenden Beispiel:

self.addEventListener('install', event => {
    event.registerForeignFetch({
    scopes: [self.registration.scope], // or some sub-scope
    origins: ['*'] // or ['https://example.com']
    });
});

Es gibt zwei Konfigurationsoptionen, die beide erforderlich sind:

  • scopes nimmt ein Array mit einem oder mehreren Strings an, die jeweils einen Bereich für Anfragen darstellen, durch die ein foreignfetch-Ereignis ausgelöst wird. Aber Moment, denken Sie vielleicht, ich habe bereits bei der Registrierung des Service Workers einen Gültigkeitsbereich definiert! Das ist richtig und dieser Gesamtumfang ist weiterhin relevant. Jeder hier angegebene Umfang muss entweder mit dem Gesamtumfang des Service Workers übereinstimmen oder ein untergeordneter Umfang sein. Mit den zusätzlichen Einschränkungen für den Geltungsbereich können Sie einen universellen Dienst-Worker bereitstellen, der sowohl fetch-Ereignisse von selbst erhobenen Daten (für Anfragen von Ihrer eigenen Website) als auch foreignfetch-Ereignisse von Drittanbietern (für Anfragen von anderen Domains) verarbeiten kann. Außerdem können Sie festlegen, dass foreignfetch nur von einem Teil Ihres größeren Geltungsbereichs ausgelöst werden soll. Wenn Sie einen Service Worker bereitstellen, der nur foreignfetch-Ereignisse von Drittanbietern verarbeiten soll, sollten Sie in der Praxis nur einen einzelnen, expliziten Gültigkeitsbereich verwenden, der dem Gesamtumfang Ihres Service Workers entspricht. Das ist im Beispiel oben der Fall, in dem der Wert self.registration.scope verwendet wird.
  • origins akzeptiert auch ein Array mit einem oder mehreren Strings und ermöglicht es Ihnen, Ihren foreignfetch-Handler so einzuschränken, dass er nur auf Anfragen von bestimmten Domains reagiert. Wenn Sie beispielsweise „https://beispiel.de“ explizit zulassen, wird Ihr Handler für externe Abrufe ausgelöst, wenn von einer Seite, die auf https://example.com/path/to/page.html gehostet wird, eine Anfrage für eine Ressource gestellt wird, die aus Ihrem Bereich für externe Abrufe bereitgestellt wird. Anfragen von https://random-domain.com/path/to/page.html lösen Ihren Handler jedoch nicht aus. Sofern Sie keinen bestimmten Grund haben, Ihre Logik für den externen Abruf nur für einen Teil der Remote-Quellen auszulösen, können Sie einfach '*' als einzigen Wert im Array angeben. In diesem Fall sind alle Quellen zulässig.

Der Event-Handler „foreignfetch“

Nachdem Sie den Service Worker des Drittanbieters installiert und über registerForeignFetch() konfiguriert haben, kann er Subressourcenanforderungen zwischen verschiedenen Ursprüngen an Ihren Server abfangen, die in den Bereich des externen Abrufs fallen.

Bei einem herkömmlichen Service Worker von Erstanbietern würde jede Anfrage ein fetch-Ereignis auslösen, auf das Ihr Service Worker reagieren könnte. Unser Drittanbieter-Dienst-Worker kann ein etwas anderes Ereignis namens foreignfetch verarbeiten. Konzeptionell sind die beiden Ereignisse sehr ähnlich und bieten Ihnen die Möglichkeit, die eingehende Anfrage zu prüfen und optional über respondWith() eine Antwort darauf zu geben:

self.addEventListener('foreignfetch', event => {
    // Assume that requestLogic() is a custom function that takes
    // a Request and returns a Promise which resolves with a Response.
    event.respondWith(
    requestLogic(event.request).then(response => {
        return {
        response: response,
        // Omit to origin to return an opaque response.
        // With this set, the client will receive a CORS response.
        origin: event.origin,
        // Omit headers unless you need additional header filtering.
        // With this set, only Content-Type will be exposed.
        headers: ['Content-Type']
        };
    })
    );
});

Trotz der konzeptionellen Ähnlichkeiten gibt es einige praktische Unterschiede beim Aufrufen von respondWith() auf einem ForeignFetchEvent. Anstatt einfach einen Response (oder einen Promise, der in einen Response aufgelöst wird) für respondWith() anzugeben, wie du es bei einem FetchEvent tust, musst du einen Promise übergeben, der in ein Objekt mit bestimmten Properties für die respondWith() des ForeignFetchEvent aufgelöst wird:

  • response ist erforderlich und muss auf das Response-Objekt festgelegt werden, das an den Client zurückgegeben wird, der die Anfrage gestellt hat. Wenn du etwas anderes als eine gültige Response angibst, wird die Anfrage des Clients mit einem Netzwerkfehler beendet. Im Gegensatz zum Aufruf von respondWith() in einem fetch-Ereignis-Handler müssen Sie hier eine Response angeben, keine Promise, die auf eine Response verweist. Sie können Ihre Antwort über eine Promise-Kette erstellen und diese Kette als Parameter an foreignfetchs respondWith() übergeben. Die Kette muss jedoch in ein Objekt aufgelöst werden, das die Eigenschaft response enthält, die auf ein Response-Objekt festgelegt ist. Eine Demonstration dazu finden Sie im Codebeispiel oben.
  • origin ist optional und wird verwendet, um zu bestimmen, ob die zurückgegebene Antwort undurchsichtig ist. Wenn Sie dies weglassen, ist die Antwort undurchsichtig und der Client hat nur eingeschränkten Zugriff auf den Text und die Header der Antwort. Wenn die Anfrage mit mode: 'cors' erfolgt, wird die Rückgabe einer nicht transparenten Antwort als Fehler behandelt. Wenn Sie jedoch einen Stringwert angeben, der dem Ursprung des Remote-Clients entspricht (der über event.origin abgerufen werden kann), aktivieren Sie ausdrücklich die Bereitstellung einer CORS-kompatiblen Antwort für den Client.
  • headers ist ebenfalls optional und nur nützlich, wenn du auch origin angibst und eine CORS-Antwort zurückgibst. Standardmäßig sind nur Header in der Liste der CORS-Safelist-Antwortheader in der Antwort enthalten. Wenn Sie die zurückgegebenen Daten weiter filtern möchten, können Sie eine Liste mit einem oder mehreren Headernamen angeben. Diese Liste wird dann als Zulassungsliste für die Header verwendet, die in der Antwort angezeigt werden sollen. So können Sie CORS aktivieren und gleichzeitig verhindern, dass potenziell sensible Antwortheader direkt für den Remote-Client freigegeben werden.

Wichtig ist, dass der foreignfetch-Handler beim Ausführen Zugriff auf alle Anmeldedaten und die ambient authority des Ursprungs hat, der den Service Worker hostet. Als Entwickler, der einen externen, für das Abrufen aktivierten Dienst-Worker implementiert, liegt es in Ihrer Verantwortung, dafür zu sorgen, dass keine privilegierten Antwortdaten gehackt werden, die andernfalls aufgrund dieser Anmeldedaten nicht verfügbar wären. Die Einwilligung für CORS-Antworten ist ein Schritt, um unbeabsichtigte Offenlegungen zu begrenzen. Als Entwickler können Sie jedoch fetch()-Anfragen in Ihrem foreignfetch-Handler explizit stellen, die keine impliziten Anmeldedaten verwenden:

self.addEventListener('foreignfetch', event => {
    // The new Request will have credentials omitted by default.
    const noCredentialsRequest = new Request(event.request.url);
    event.respondWith(
    // Replace with your own request logic as appropriate.
    fetch(noCredentialsRequest)
        .catch(() => caches.match(noCredentialsRequest))
        .then(response => ({response}))
    );
});

Überlegungen für Kunden

Es gibt einige zusätzliche Aspekte, die sich darauf auswirken, wie der Worker des externen Abrufdiensts Anfragen von Clients Ihres Dienstes verarbeitet.

Clients mit eigenem Service Worker

Einige Kunden Ihres Dienstes haben möglicherweise bereits einen eigenen Service Worker, der Anfragen verarbeitet, die von ihrer Webanwendung stammen. Was bedeutet das für Ihren externen Service Worker für den Abruf?

Die fetch-Handler in einem eigenen Service Worker erhalten die erste Gelegenheit, auf alle Anfragen der Webanwendung zu reagieren, auch wenn ein Drittanbieter-Service Worker mit aktivierter foreignfetch und einem Umfang vorhanden ist, der die Anfrage abdeckt. Clients mit eigenen Service Workern können jedoch weiterhin Ihren externen Fetch-Service Worker nutzen.

Wenn Sie in einem selbstverwalteten Service Worker fetch() zum Abrufen plattformübergreifender Ressourcen verwenden, wird der entsprechende externe Dienst-Worker für das Abrufen ausgelöst. Das bedeutet, dass Code wie der folgende von Ihrem foreignfetch-Handler profitieren kann:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    // If event.request is under your foreign fetch service worker's
    // scope, this will trigger your foreignfetch handler.
    event.respondWith(fetch(event.request));
});

Wenn es auch Abruf-Handler von Drittanbietern gibt, die event.respondWith() aber nicht beim Abwickeln von Anfragen für Ihre ressourcenübergreifende Ressource aufrufen, wird die Anfrage automatisch an Ihren foreignfetch-Handler weitergeleitet:

// Inside a client's first-party service-worker.js:
self.addEventListener('fetch', event => {
    if (event.request.mode === 'same-origin') {
    event.respondWith(localRequestLogic(event.request));
    }

    // Since event.respondWith() isn't called for cross-origin requests,
    // any foreignfetch handlers scoped to the request will get a chance
    // to provide a response.
});

Wenn ein fetch-Handler von Drittanbietern event.respondWith() aufruft, aber fetch() nicht verwendet, um eine Ressource im Bereich des externen Abrufs anzufordern, kann der Service Worker für den externen Abruf die Anfrage nicht verarbeiten.

Clients ohne eigenen Service Worker

Alle Clients, die Anfragen an einen Drittanbieterdienst senden, können davon profitieren, wenn der Dienst einen externen Fetch-Dienst-Worker bereitstellt, auch wenn sie noch keinen eigenen Dienst-Worker verwenden. Clients müssen nichts Bestimmtes tun, um die Verwendung eines externen Fetch-Dienst-Workers zu aktivieren, solange sie einen Browser verwenden, der dies unterstützt. Wenn Sie also einen externen Fetch-Dienstworker bereitstellen, profitieren viele der Kunden Ihres Dienstes sofort von Ihrer benutzerdefinierten Anfragelogik und dem gemeinsamen Cache, ohne dass sie weitere Schritte ausführen müssen.

Alles zusammenfassen: Wo Kunden nach einer Antwort suchen

Unter Berücksichtigung der oben genannten Informationen können wir eine Hierarchie von Quellen erstellen, die ein Client verwendet, um eine Antwort für eine plattformübergreifende Anfrage zu finden.

  1. fetch-Handler eines eigenen Service Workers (falls vorhanden)
  2. Der foreignfetch-Handler eines Drittanbieter-Diensteworkers (falls vorhanden und nur für plattformübergreifende Anfragen)
  3. Der HTTP-Cache des Browsers (falls eine aktuelle Antwort vorhanden ist)
  4. Das Netzwerk

Der Browser beginnt oben und geht je nach Service Worker-Implementierung die Liste so lange durch, bis er eine Quelle für die Antwort findet.

Weitere Informationen

Bleiben Sie auf dem Laufenden

Die Implementierung des Ursprungstests für externe Abrufe in Chrome kann sich ändern, da wir das Feedback von Entwicklern berücksichtigen. Wir halten diesen Beitrag über Inline-Änderungen auf dem neuesten Stand und notieren die konkreten Änderungen unten, sobald sie in Kraft treten. Informationen zu wichtigen Änderungen werden auch über das Twitter-Konto @chromiumdev veröffentlicht.