PWA bei Google erstellen, Teil 1

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

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

Dies ist der erste Blogpost einer Reihe von Blogposts, in denen es um die Erkenntnisse geht, die das Google Bulletin-Team beim Erstellen einer externen PWA gelernt hat. In diesen Beiträgen teilen wir einige unserer Herausforderungen, die Ansätze zur Bewältigung dieser Probleme und allgemeine Tipps zur Vermeidung von Fallstricken auf. Dabei handelt es sich jedoch bei Weitem nicht um einen vollständigen Überblick über PWAs. Ziel ist es, die aus den Erfahrungen unseres Teams gewonnenen Erkenntnisse weiterzugeben.

In diesem ersten Beitrag behandeln wir zuerst einige Hintergrundinformationen und gehen dann auf alles ein, was wir über Service Worker gelernt haben.

Hintergrund

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

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

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

  • Fähigkeit, schnelle Iterationen durchzuführen. Dies ist besonders wichtig, da Bulletin in mehreren Märkten getestet werden würde.
  • Einzelne Codebasis: Die Nutzer wurden etwa gleichmäßig zwischen Android und iOS aufgeteilt. Mit einer PWA konnten wir eine 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 Back-End-Ä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 dazu oft einfach eine URL geöffnet werden.
  • Die Installation einer App ist jetzt einfacher.

Unser Framework

Für Bulletin haben wir Polymer verwendet, aber jedes moderne, gut unterstützte Framework funktioniert.

Was wir über Service Worker gelernt haben

Sie können eine PWA ohne Service-Worker nicht haben. Service Worker bieten Ihnen viel Leistung, z. B. erweiterte Caching-Strategien, Offlinefunktionen, Hintergrundsynchronisierung usw. Auch wenn Service Worker zwar etwas komplexer sind, haben wir jedoch herausgefunden, dass ihre Vorteile die zusätzliche Komplexität überwiegen.

Wenn möglich, generieren Sie ihn.

Vermeiden Sie es, ein Service Worker-Skript von Hand zu schreiben. Wenn Sie Service Worker manuell schreiben möchten, müssen Sie im Cache gespeicherte Ressourcen manuell verwalten und die Logik neu schreiben, die bei den meisten Service-Worker-Bibliotheken wie Workbox üblich ist.

Aufgrund unseres internen Technologie-Stacks konnten wir jedoch keine Bibliothek verwenden, um unseren Service Worker zu generieren und zu verwalten. Die nachfolgenden Erkenntnisse spiegeln dies gelegentlich wider. Weitere Informationen finden Sie unter Fallstricke für nicht generierte Service Worker.

Nicht alle Bibliotheken sind mit Service-Workern 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 Service-Workern nicht zur Verfügung stehen (XMLHttpRequest, lokaler Speicher usw.). Achten Sie darauf, dass alle kritischen Bibliotheken, die Sie für Ihre Anwendung benötigen, mit Service-Workern kompatibel sind. Für diese PWA wollten wir gapi.js zur Authentifizierung verwenden, konnten dies jedoch nicht tun, da Service Worker nicht unterstützt werden. Außerdem sollten Bibliotheksautoren nach Möglichkeit unnötige Annahmen über den JavaScript-Kontext entfernen oder entfernen, um Service Worker-Anwendungsfälle zu unterstützen. Dazu gehören z. B. nicht kompatible APIs und globale Status.

Zugriff auf IndexedDB während der Initialisierung vermeiden

Lesen Sie IndexedDB nicht, wenn Sie Ihr Service Worker-Skript initialisieren, da Sie sonst in diese unerwünschte Situation geraten können:

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

In unserem Fall wurde der Cache bei der Service-Worker-Installation entwertet. Wenn der Service Worker also nie installiert wurde, erhielten die Nutzer die aktualisierte App nicht.

Stabilisieren

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

Bei einem Synchronisierungsprozess, 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 Uploadbibliothek vor dem Upload in IDB zu speichern und diese URL zu verwenden, um einen Upload fortzusetzen, 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, an welcher Stelle im Prozess wir sich 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- als auch in einem Service Worker-Kontext ausgeführt (z. B. Logging, Flags, Synchronisierung). Code muss die verwendeten Dienste, z. B. den lokalen Speicher oder Cookies, zurückhalten. Mit globalThis können Sie in allen Kontexten auf das globale Objekt verweisen. Verwenden Sie Daten, die in globalen Variablen gespeichert sind, 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. Während der Entwicklung ist dies jedoch genau das Gegenteil dessen, was Sie sich wünschen, insbesondere wenn Aktualisierungen verzögert ausgeführt werden. Der Server-Worker sollte weiterhin installiert sein, damit Sie Probleme damit beheben oder mit anderen APIs wie Hintergrundsynchronisierung oder Benachrichtigungen arbeiten können. In Chrome können Sie dies mithilfe der Chrome-Entwicklertools tun. Aktivieren Sie dazu das Kästchen Für Netzwerk umgehen (Bereich Anwendung > Bereich Dienstmitarbeiter) und im Bereich Netzwerk das Kästchen Cache deaktivieren, um auch den Arbeitsspeicher-Cache zu deaktivieren. Um mehr Browser abzudecken, entschieden wir uns für eine andere Lösung. Dazu gehörten ein Flag zur Deaktivierung des Caching in unserem Service Worker, das bei Entwickler-Builds standardmäßig aktiviert ist. So erhalten Entwickler immer die neuesten Änderungen ohne Caching-Probleme. Es ist wichtig, auch den Header Cache-Control: no-cache einzufügen, um zu verhindern, dass der Browser Assets im Cache speichert.

Leuchtturm

Lighthouse bietet eine Reihe von Debugging-Tools, die für PWAs nützlich sind. Dabei wird eine Website gescannt und Berichte zu PWAs, Leistung, Barrierefreiheit, SEO und anderen Best Practices erstellt. Wir empfehlen, Lighthouse der kontinuierlichen Integration auszuführen, um dich zu benachrichtigen, wenn eines der Kriterien für eine PWA nicht erfüllt wird. Das ist uns einmal passiert, als der Service Worker nicht installiert hat und wir es vor einem Produktions-Push nicht bemerkt haben. Mit Lighthouse als Teil unserer CI wäre das verhindert worden.

Auf Continuous Delivery setzen

Da Service Worker automatische Updates durchführen können, haben Nutzer nicht die Möglichkeit, Upgrades einzuschränken. Dadurch wird die Anzahl veralteter Clients erheblich reduziert. Als der Nutzer unsere App öffnete, bediente der Service Worker den alten Client, während der neue Client verzögert heruntergeladen wurde. Nachdem der neue Client heruntergeladen wurde, wird der Nutzer aufgefordert, die Seite zu aktualisieren, um auf neue Funktionen zuzugreifen. Selbst wenn der Nutzer diese Anfrage ignoriert, erhält er bei der nächsten Aktualisierung der Seite die neue Version des Clients. Daher ist es für Nutzer recht schwierig, Updates abzulehnen, wie es bei iOS-/Android-Apps der Fall ist.

Wir konnten funktionsgefährdende Back-End-Änderungen mit einer sehr kurzen Migrationszeit für Clients vornehmen. Normalerweise geben wir Nutzern einen Monat Zeit, um auf neuere Clients zu aktualisieren, bevor wichtige Änderungen vorgenommen werden. Da die Anwendung zwar veraltet wäre, wären ältere Clients tatsächlich verfügbar, wenn der Nutzer die Anwendung längere Zeit nicht geöffnet hätte. Unter iOS werden Service Worker nach einigen Wochen entfernt, sodass dieser Fall nicht auftritt. Für Android kann dieses Problem gemindert werden, indem die Bereitstellung inaktiv ist oder der Inhalt nach einigen Wochen manuell abläuft. In der Praxis sind Probleme durch veraltete Clients aufgetreten. Wie streng ein bestimmtes Team hier sein möchte, hängt vom jeweiligen Anwendungsfall ab. PWAs bieten jedoch deutlich mehr Flexibilität als iOS-/Android-Apps.

Cookiewerte in einem Service Worker abrufen

Manchmal ist es erforderlich, in einem Service Worker-Kontext auf Cookie-Werte zuzugreifen. In unserem Fall mussten wir auf Cookiewerte zugreifen, um ein Token zur Authentifizierung von Erstanbieter-API-Anfragen zu generieren. In einem Service Worker sind synchrone APIs wie document.cookies nicht verfügbar. Sie können immer eine Nachricht vom Service Worker an aktive Clients (mit Fenster) senden, um die Cookiewerte anzufordern. Es ist jedoch möglich, dass der Service Worker im Hintergrund ausgeführt wird, ohne dass Clients im Fenstermodus verfügbar sind, z. B. während einer Hintergrundsynchronisierung. Um dieses Problem zu umgehen, haben wir auf unserem Frontend-Server einen Endpunkt erstellt, der einfach den Cookiewert an den Client zurücksendet. 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 asynchronen Zugriff auf Browser-Cookies bietet 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 besteht darin, dass ein Service Worker in der install-Phase alle statischen Anwendungsdateien installiert. Dadurch können Clients bei allen nachfolgenden Besuchen direkt auf den Cache Storage API-Cache zugreifen. Service Worker werden nur installiert, wenn der Browser feststellt, dass das Service-Worker-Script geändert wurde. Daher mussten wir sicherstellen, dass die Service Worker-Skriptdatei selbst bei einer Änderung einer im Cache gespeicherten Datei geändert wurde. Dazu haben wir manuell einen Hash des Dateisatzes statischer Ressourcen in unser Service Worker-Skript eingebettet, sodass für jeden Release eine eigene Service Worker-JavaScript-Datei erzeugt wurde. Service Worker-Bibliotheken wie Workbox automatisieren diesen Prozess.

Unittest

Für die Funktion der Service Worker APIs werden dem globalen Objekt Ereignis-Listener hinzugefügt. Beispiel:

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

Dies kann mühsam sein, da Sie den Ereignistrigger und das Ereignisobjekt simulieren, auf den respondWith()-Callback warten und dann auf das Versprechen warten müssen, bevor Sie schließlich das Ergebnis durchsetzen. Eine einfachere Möglichkeit, dies zu strukturieren, besteht darin, alle Implementierungen an eine andere Datei zu delegieren, die sich leichter testen lässt.

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

Aufgrund der Schwierigkeiten beim Testen der Einheiten eines Service Worker-Skripts haben wir das Core Service Worker-Skript so einfach wie möglich gehalten und den größten Teil der Implementierung in andere Module aufgeteilt. Da es sich bei diesen Dateien nur um Standard-JS-Module handelt, können sie einfacher mit Standard-Testbibliotheken getestet werden.

Mehr dazu demnächst mit Teil 2 und 3

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