PWA bei Google erstellen, Teil 1

Was das Bulletin-Team bei der Entwicklung einer PWA über Service Worker erfahren hat

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

Dies ist der erste von einer Reihe von Blogposts zu den Erkenntnissen, die das Google Bulletin-Team beim Erstellen einer extern zugänglichen PWA gelernt hat. In diesen Posts stellen wir einige der Herausforderungen vor, mit denen wir konfrontiert waren, die Ansätze, die wir zu deren Bewältigung gewählt haben, und allgemeine Ratschläge zur Vermeidung von Fallstricken. Dies ist keineswegs ein vollständiger Überblick über PWAs. Ziel ist es, die Erfahrungen unseres Teams zu teilen.

In diesem ersten Beitrag behandeln wir zuerst einige Hintergrundinformationen und werfen dann einen genaueren Blick auf das, was wir über Service Worker gelernt haben.

Hintergrund

Bulletin war von Mitte 2017 bis Mitte 2019 in der Entwicklung.

Warum wir uns für die Entwicklung einer PWA entschieden haben

Bevor wir uns mit dem Entwicklungsprozess befassen, untersuchen wir, warum das Erstellen einer PWA für dieses Projekt eine attraktive Option war:

  • Möglichkeit zur schnellen Iteration. Dies ist besonders nützlich, da Bulletin in mehreren Märkten getestet werden sollte.
  • Einzelne Codebasis. Unsere Nutzer verteilten sich ungefähr gleichmäßig auf Android und iOS. Mit einer PWA konnten wir eine einzige Web-App entwickeln, die auf beiden Plattformen funktioniert. Dies erhöhte die Geschwindigkeit und die Wirkung des Teams.
  • Schnelle Aktualisierung und unabhängig vom Nutzerverhalten. PWAs können automatisch aktualisiert werden, was die Anzahl veralteter Clients reduziert. Wir konnten funktionsgefährdende Backend-Änderungen mit sehr kurzer Migrationszeit für Clients vornehmen.
  • Einfache Einbindung in eigene und Drittanbieter-Apps Solche Integrationen waren eine Voraussetzung für die App. Bei einer PWA musste oft einfach eine URL geöffnet werden.
  • Die Installation einer App wurde vereinfacht.

Unser Framework

Für Bulletin haben wir Polymer verwendet, aber jedes moderne, unterstützte Framework kann dafür verwendet werden.

Was wir über Service Worker gelernt haben

Eine PWA kann ohne einen Dienst-Worker nicht verwendet werden. Service Worker bieten Ihnen eine Menge Leistung, z. B. erweiterte Caching-Strategien, Offlinefunktionen, Hintergrundsynchronisierung usw. Obwohl Service Worker eine gewisse Komplexität erhöhen, haben wir festgestellt, dass ihre Vorteile die zusätzliche Komplexität überwiegen.

Generieren Sie sie, wenn möglich.

Vermeiden Sie es, ein Service Worker-Skript von Hand zu schreiben. Das manuelle Schreiben von Service Workern erfordert die manuelle Verwaltung von im Cache gespeicherten Ressourcen und das Umschreiben der Logik, die für die meisten Service Worker-Bibliotheken üblich ist, z. B. Workbox.

Allerdings konnten wir aufgrund unseres internen Technologie-Stacks keine Bibliothek verwenden, um unseren Service Worker zu generieren und zu verwalten. Unsere Erkenntnisse unten werden dies gelegentlich widerspiegeln. Weitere Informationen finden Sie unter Fallstricke für nicht generierte Service Worker.

Nicht alle Bibliotheken sind Service-Worker-kompatibel

Einige JS-Bibliotheken gehen von Annahmen aus, die nicht wie erwartet funktionieren, wenn sie von einem Service Worker ausgeführt werden. Angenommen, window oder document sind verfügbar oder Sie verwenden eine API, die für Dienst-Worker nicht verfügbar ist (XMLHttpRequest, lokaler Speicher usw.). Prüfen Sie, ob alle wichtigen Bibliotheken, die Sie für Ihre Anwendung benötigen, mit Service-Workern kompatibel sind. Für diese spezielle PWA wollten wir zur Authentifizierung gapi.js verwenden. Dies war jedoch nicht möglich, da sie keine Dienst-Worker unterstützt. Bibliotheksautoren sollten auch unnötige Annahmen zum JavaScript-Kontext reduzieren oder entfernen, um Service Worker-Anwendungsfälle zu unterstützen. Dazu gehören beispielsweise die Vermeidung von Service Worker-inkompatiblen APIs und der Vermeidung globaler Status.

Zugriff auf IndexedDB während der Initialisierung vermeiden

Lesen Sie IndexedDB nicht beim Initialisieren Ihres Service Worker-Skripts, da sonst dieses unerwünschte Problem auftritt:

  1. Der Nutzer hat eine Web-App mit IndexedDB (IDB) Version N
  2. Neue Webanwendung wird mit IDB-Version N+1 bereitgestellt
  3. Der Nutzer besucht die PWA. Dadurch wird der neue Service Worker heruntergeladen.
  4. Neuer Service Worker liest aus IDB, bevor der Event-Handler install registriert wird, wodurch ein IDB-Upgradezyklus von N auf N+1 ausgelöst wird.
  5. Da der Nutzer einen alten Client mit Version N hat, hängt der Service Worker-Upgradeprozess auf, da aktive Verbindungen noch offen für die alte Version der Datenbank sind.
  6. Service Worker hängt und installiert nie

In unserem Fall wurde der Cache bei der Installation des Service Workers ungültig gemacht. Wenn der Service Worker also nicht installiert wurde, erhalten die Nutzer die aktualisierte Anwendung nicht.

Resilienz

Obwohl Service Worker-Skripts im Hintergrund ausgeführt werden, können sie jederzeit beendet werden, selbst während sie sich inmitten von E/A-Vorgängen (Netzwerk, IDB usw.) befinden. Prozesse mit langer Ausführungszeit sollten jederzeit fortgesetzt werden können.

Im Falle eines Synchronisierungsprozesses, bei dem große Dateien auf den Server hochgeladen und in IDB gespeichert wurden, bestand unsere Lösung für unterbrochene Teiluploads darin, das fortsetzbare System unserer internen Upload-Bibliothek zu nutzen, die fortsetzbare Upload-URL vor dem Upload in IDB zu speichern und diese URL zum Fortsetzen eines Uploads zu verwenden, wenn er beim ersten Mal nicht abgeschlossen wurde. Außerdem wurde der Status vor einem lang andauernden E/A-Vorgang in IDB gespeichert, um anzugeben, wo im Prozess wir uns für jeden Datensatz befanden.

Keine Abhängigkeit vom globalen Status

Da Service Worker in einem anderen Kontext existieren, sind viele von Ihnen erwartete Symbole nicht vorhanden. Ein Großteil unseres Codes wurde sowohl in einem window-Kontext als auch in einem Service Worker-Kontext wie Logging, Flags, Synchronisierung usw. ausgeführt. Code muss die von ihm verwendeten Dienste wie lokalen Speicher oder Cookies schützen. Mit globalThis können Sie auf eine Weise auf das globale Objekt verweisen, die in allen Kontexten funktioniert. Verwenden Sie außerdem in globalen Variablen gespeicherte Daten sparsam, da es keine Garantie dafür gibt, wann das Skript beendet und der Status entfernt wird.

Lokale Entwicklung

Eine wichtige Komponente von Service Workern ist das lokale Caching von Ressourcen. In der Entwicklungsphase ist dies jedoch genau das Gegenteil von dem, was Sie möchten, insbesondere wenn Aktualisierungen verzögert erfolgen. Der Server-Worker soll dennoch installiert sein, damit Sie Probleme damit beheben oder andere APIs wie die Hintergrundsynchronisierung oder Benachrichtigungen verwenden können. In Chrome ist dies über die Chrome-Entwicklertools möglich. Aktivieren Sie dazu im Bereich Netzwerk das Kästchen Für Netzwerk umgehen (Bereich Anwendung > Bereich Dienstarbeiter) und aktivieren Sie zusätzlich das Kästchen Cache deaktivieren, um auch den Arbeitsspeicher-Cache zu deaktivieren. Um mehr Browser abzudecken, haben wir uns für eine andere Lösung entschieden. Dazu haben wir ein Flag eingefügt, mit dem das Caching in unserem Service Worker deaktiviert wird. Dieses Flag ist bei Entwickler-Builds standardmäßig aktiviert. So erhalten Entwickler immer die neuesten Änderungen ohne Caching-Probleme. Außerdem muss der Header Cache-Control: no-cache enthalten sein, damit verhindert wird, dass der Browser Assets im Cache speichert.

Leuchtturm

Lighthouse bietet eine Reihe von Debugging-Tools für PWAs. Dabei wird eine Website gescannt und Berichte zu PWAs, Leistung, Barrierefreiheit, SEO und anderen Best Practices erstellt. Wir empfehlen, Lighthouse für kontinuierliche Integration auszuführen, damit Sie benachrichtigt werden, wenn Sie eines der Kriterien für eine PWA nicht erfüllen. Dies ist einmal passiert, als der Service Worker nicht installiert wurde und wir das vor einem Produktions-Push nicht bemerkt haben. Mit Lighthouse wäre das verhindert.

Continuous Delivery nutzen

Da Service Worker automatisch Updates ausführen können, haben Nutzer keine Möglichkeit, Upgrades zu begrenzen. Dies reduziert die Anzahl veralteter Clients erheblich. Wenn der Nutzer unsere App öffnete, bediente der Service Worker den alten Client, während er nach und nach den neuen Client herunterlud. Nachdem der neue Client heruntergeladen wurde, wird der Nutzer aufgefordert, die Seite zu aktualisieren, um auf neue Funktionen zugreifen zu können. Auch wenn der Nutzer diese Anfrage ignoriert hat, erhält er bei der nächsten Aktualisierung der Seite die neue Version des Clients. Daher ist es für Nutzer sehr schwierig, Updates auf dieselbe Weise abzulehnen wie für iOS-/Android-Apps.

Wir konnten funktionsgefährdende Backend-Änderungen mit sehr kurzer Migrationszeit für Clients vornehmen. In der Regel haben Nutzer einen Monat Zeit, auf neuere Clients zu aktualisieren, bevor wichtige Änderungen vorgenommen werden. Da die Anwendung veraltet war, konnten auch ältere Clients tatsächlich existieren, wenn der Nutzer die Anwendung lange Zeit nicht geöffnet hatte. Unter iOS werden Service Worker nach einigen Wochen entfernt, sodass dieser Fall nicht passiert. Bei Android könnte dieses Problem behoben werden, indem die Inhalte nicht bereitgestellt werden, wenn sie veraltet sind, oder die Inhalte nach einigen Wochen manuell ablaufen. In der Praxis sind wir nie auf Probleme mit veralteten Clients gestoßen. Wie streng ein bestimmtes Team dabei sein möchte, hängt vom jeweiligen Anwendungsfall ab. PWAs bieten jedoch deutlich mehr Flexibilität als iOS-/Android-Apps.

Cookie-Werte in einem Service Worker abrufen

Manchmal ist es erforderlich, in einem Service Worker-Kontext auf Cookiewerte zuzugreifen. In unserem Fall mussten wir auf Cookiewerte zugreifen, um ein Token zu generieren, um API-Anfragen von Erstanbietern zu authentifizieren. In einem Service Worker sind keine synchronen APIs wie document.cookies verfügbar. Sie können vom Service Worker jederzeit eine Nachricht an aktive Clients (Fenstermodus) senden, um die Cookiewerte anzufordern. Allerdings kann der Service Worker auch ohne verfügbare Fensterclients im Hintergrund ausgeführt werden, beispielsweise bei einer Hintergrundsynchronisierung. Zur Umgehung dieses Problems haben wir einen Endpunkt auf unserem Frontend-Server erstellt, der einfach den Cookiewert an den Client zurückgibt. Der Service Worker hat eine Netzwerkanfrage an diesen Endpunkt gesendet und die Antwort gelesen, um die Cookiewerte abzurufen.

Mit der Veröffentlichung der Cookie Store API sollte diese Problemumgehung für Browser, die diese API unterstützen, nicht mehr erforderlich sein, da sie einen asynchronen Zugriff auf Browsercookies ermöglicht und direkt vom Service Worker verwendet werden kann.

Fallstricke für nicht generierte Service Worker

Achten Sie darauf, dass sich das Service Worker-Skript ändert, wenn sich eine statische im Cache gespeicherte Datei ändert

Ein gängiges PWA-Muster ist, dass ein Service Worker während der install-Phase alle statischen Anwendungsdateien installiert. Dadurch können Clients den Cache Storage API-Cache für alle nachfolgenden Besuche direkt aufrufen. Service Worker werden nur installiert, wenn der Browser feststellt, dass sich das Service Worker-Skript geändert hat. Daher mussten wir darauf achten, dass die Service Worker-Skriptdatei selbst bei Änderung einer im Cache gespeicherten Datei geändert wurde. Dazu haben wir manuell einen Hash des Dateisatzes der statischen Ressourcen in unser Service Worker-Skript eingebettet, sodass mit jedem Release eine eigene Service Worker-JavaScript-Datei erstellt wurde. Service Worker-Bibliotheken wie Workbox automatisieren diesen Prozess für Sie.

Unittest

Service Worker APIs funktionieren, indem dem globalen Objekt Ereignis-Listener hinzugefügt werden. Beispiel:

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

Das kann mühsam sein, da Sie den Ereignistrigger bzw. das Ereignisobjekt simulieren, auf den respondWith()-Callback warten und dann auf das Versprechen warten müssen, bevor Sie das Ergebnis durchsetzen. Eine einfachere Möglichkeit zur Strukturierung besteht darin, die gesamte Implementierung an eine andere Datei zu delegieren, was leichter zu testen ist.

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

Aufgrund der Schwierigkeiten beim Testen eines Service Worker-Skripts haben wir das Skript für das Core Service Worker-Skript so einfach wie möglich gehalten und den Großteil der Implementierung in andere Module aufgeteilt. Da es sich bei diesen Dateien nur um Standard-JS-Module handelte, war es einfacher, sie mit Standardtestbibliotheken zu testen.

Demnächst folgen Teil 2 und 3

In den Teilen 2 und 3 dieser Reihe geht es um die Medienverwaltung und iOS-spezifische Probleme. Wenn Sie mehr über die Entwicklung einer PWA bei Google erfahren möchten, besuchen Sie unsere Autorenprofile. Dort erfahren Sie, wie Sie uns kontaktieren können: