Cross-Origin Service Workers – Tests mit Foreign Fetch

Hintergrund

Mit Service Workern haben Webentwickler die Möglichkeit, auf Netzwerkanfragen ihrer Webanwendungen zu reagieren, damit sie auch offline weiterarbeiten können. Außerdem können sie Lie-Fi bekämpfen und komplexe Cache-Interaktionen wie veraltete Neuvalidierung implementieren. 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
  • Analyseanbieter
  • Anbieter für Bildhosting
  • 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 ist zwar möglich, dass die Clients eines Dienstes ein ähnliches Verhalten über eigene Service Worker implementieren. Es ist jedoch nicht so skalierbar, wenn jeder Client eine spezielle Logik für Ihren Dienst schreiben muss, als die Verwendung eines von Ihnen bereitgestellten externen Service Workers für den Fremdabruf.

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 sollte als HTTP-Antwortheader in allen ursprungsübergreifenden Anfragen für Ressourcen enthalten sein, die Sie per fremdem Abruf 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. Wir gehen davon aus, dass wir alle notwendigen Änderungen zur Stabilisierung der Funktion gefunden und diese (hoffentlich) standardmäßig aktiviert haben. 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.

Um vor der Registrierung für ein offizielles Ursprungstesttoken das Experimentieren mit ausländischen Abrufen zu erleichtern, 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 das Flag „Experimental Web Platform features“. 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

Wenn die Voraussetzungen erledigt sind, schauen wir uns jetzt die technischen Details an, die erforderlich sind, um einen fremden Abruf-Service-Worker zum Laufen zu bringen.

Service Worker registrieren

Die erste Herausforderung, auf die Sie wahrscheinlich stoßen, 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.

Die externe Abruffunktion ist derzeit als Ursprungstest implementiert. Daher müssen Sie neben dem Link-Antwortheader auch einen gültigen Origin-Trial-Header angeben. Die Mindestanzahl von Antwortheadern, die zum Registrieren des externen Fetch-Dienst-Workers hinzugefügt werden müssen, 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 Service Worker für fremde Abrufe zu registrieren, müssen Sie einen Link-Header für eine Antwort auf eine in Ihrer Domain gehostete Ressource festlegen, wie 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 fremden Abruf ordnungsgemäß registriert?

Sie können auch die zugrunde liegende Service Worker-Registrierung einschließlich ihres Umfangs prüfen. Sehen Sie sich dazu die vollständige Liste der Service Worker im Anwendungsbereich der Entwicklertools an. Wählen Sie die Option „Alle anzeigen“ aus, da standardmäßig nur Dienstprogramme für den aktuellen Ursprung angezeigt werden.

Der fremde Abruf-Dienst-Worker im Anwendungsbereich

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 verwendet ein Array mit einem oder mehreren Strings, von denen jeder einen Bereich für Anfragen darstellt, die ein foreignfetch-Ereignis auslösen. 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 Drittanbieter-foreignfetch-Ereignisse 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 dir, den foreignfetch-Handler so zu beschränken, dass er nur auf Anfragen von bestimmten Domains antwortet. Wenn Sie beispielsweise „https://beispiel.de“ explizit zulassen, wird durch eine Anfrage von einer unter https://example.com/path/to/page.html gehosteten Seite für eine Ressource, die aus Ihrem fremden Abrufbereich bereitgestellt wird, der Handler für fremde Abrufe ausgelöst, Anfragen von https://random-domain.com/path/to/page.html jedoch nicht den Handler. 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.

In einem herkömmlichen eigenen Service Worker löst jede Anfrage ein fetch-Ereignis aus, auf das Ihr Service Worker antworten konnte. Unser Service Worker erhält die Möglichkeit, ein etwas anderes Ereignis namens foreignfetch zu verarbeiten. Konzeptionell sind die beiden Ereignisse sehr ähnlich und bieten 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 ein paar Unterschiede in der Praxis, wenn respondWith() für ein ForeignFetchEvent aufgerufen wird. 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. Anders als beim Aufrufen von respondWith() in einem fetch-Event-Handler müssen Sie hier einen Response angeben und kein Promise-Objekt, das mit einem Response aufgelöst wird. 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 diese Angabe weglassen, ist die Antwort undurchsichtig und der Client hat eingeschränkten Zugriff auf den Text und die Header der Antwort. Wenn die Anfrage mit mode: 'cors' gesendet wurde, wird die Rückgabe einer undurchsichtigen 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 werden nur Header in Ihrer Antwort berücksichtigt, die in der Liste der CORS-gesicherten Antwortheader enthalten sind. 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 vertrauliche Antwortheader direkt dem Remote-Client zugänglich gemacht werden.

Wichtig ist, dass der foreignfetch-Handler beim Ausführen Zugriff auf alle Anmeldedaten und die Umgebungsautorität des Ursprungs hat, auf dem der Dienst-Worker gehostet wird. Als Entwickler, der einen externen, abruffähigen 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 Aktivierung von CORS-Antworten ist ein Schritt, um die unbeabsichtigte Offenlegung zu verhindern. Als Entwickler können Sie jedoch explizit fetch()-Anfragen innerhalb Ihres foreignfetch-Handlers senden, bei denen nicht die impliziten Anmeldedaten verwendet werden. Das geht so:

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

Hinweise zu Clients

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

Kunden mit einem eigenen 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 von Inhalten?

Die fetch-Handler in einem Erstanbieter-Service-Worker erhalten die erste Gelegenheit, auf alle von der Webanwendung gestellten Anfragen zu antworten, auch wenn es einen Service Worker eines Drittanbieters mit aktiviertem foreignfetch und einem Bereich gibt, 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 Abruf-Service Worker 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, die keinen eigenen Service Worker haben

Alle Clients, die Anfragen an einen Drittanbieterdienst senden, können davon profitieren, wenn der Dienst einen fremden Fetch-Service-Worker bereitstellt, auch wenn sie nicht bereits einen eigenen Service 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 ausländische Abrufe in Chrome kann sich aufgrund von Feedback von Entwicklern ändern. 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.