Was das Bulletin-Team bei der Entwicklung einer PWA über Service Worker erfahren hat
Dies ist der erste von einer Reihe von Blogposts zu den Erkenntnissen, die das Google Bulletin-Team beim Erstellen einer externen PWA gewonnen hat. In diesen Beiträgen teilen wir einige der Herausforderungen, denen wir uns gestellt haben, die Ansätze, die wir zur Bewältigung gewählt haben, und allgemeine Tipps, wie Sie Fallstricke vermeiden können. 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 wurde von Mitte 2017 bis Mitte 2019 aktiv entwickelt.
Warum wir uns für eine PWA entschieden haben
Bevor wir uns mit dem Entwicklungsprozess befassen, sehen wir uns an, warum die Erstellung einer PWA für dieses Projekt eine attraktive Option war:
- Möglichkeit, schnell zu iterieren. Dies ist besonders nützlich, da Bulletin in mehreren Märkten getestet werden sollte.
- Eine einzige Codebasis Unsere Nutzer waren ungefähr gleichmäßig auf Android- und iOS-Geräte verteilt. Mit einer PWA konnten wir eine einzige Web-App entwickeln, die auf beiden Plattformen funktioniert. Dies erhöhte die Geschwindigkeit und Effektivität des Teams.
- Sie werden schnell und unabhängig vom Nutzerverhalten aktualisiert. PWAs können automatisch aktualisiert werden, wodurch die Anzahl der veralteten Clients reduziert wird. Wir konnten wichtige Backend-Änderungen mit einer sehr kurzen Migrationszeit für Kunden einführen.
- Einfache Einbindung in eigene Apps 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 ist jetzt einfacher.
Unser Framework
Für Bulletin haben wir Polymer verwendet, aber jedes moderne, gut unterstützte Framework ist geeignet.
Was wir über Service Worker gelernt haben
Eine PWA kann ohne einen Dienst-Worker nicht verwendet werden. Service Worker bieten viele Vorteile, z. B. erweiterte Caching-Strategien, Offlinefunktionen und Hintergrundsynchronisierung. Sie erhöhen zwar die Komplexität, aber wir haben festgestellt, dass die Vorteile die zusätzliche Komplexität überwiegen.
Erstellen Sie es, wenn möglich.
Schreiben Sie ein Service Worker-Script nicht manuell. 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.
Aufgrund unseres internen Tech-Stacks konnten wir jedoch keine Bibliothek zum Generieren und Verwalten unseres Service Workers verwenden. Unsere Erkenntnisse unten werden dies gelegentlich widerspiegeln. Weitere Informationen finden Sie unter Fallstricke bei nicht generierten Service Workers.
Nicht alle Bibliotheken sind mit Service Workern kompatibel
Einige JS-Bibliotheken gehen von Annahmen aus, die bei der Ausführung durch einen Service Worker nicht wie erwartet funktionieren. Das ist beispielsweise der Fall, wenn window
oder document
verfügbar sind oder eine API verwendet wird, die für Dienstarbeiter 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 bestimmte PWA wollten wir gapi.js für die Authentifizierung verwenden, konnten dies aber nicht, da es keine Dienst-Worker unterstützt. Bibliotheksautoren sollten außerdem nach Möglichkeit unnötige Annahmen über den JavaScript-Kontext reduzieren oder entfernen, um Anwendungsfälle für Dienstprogramme zu unterstützen. Dazu gehört beispielsweise, APIs zu vermeiden, die nicht mit Dienstprogrammen kompatibel sind, und den globalen Status zu vermeiden.
Zugriff auf IndexedDB während der Initialisierung vermeiden
Lesen Sie beim Initialisieren Ihres Service Worker-Scripts keine IndexedDB-Daten, da sonst diese unerwünschte Situation auftreten kann:
- Der Nutzer hat eine Webanwendung mit IndexedDB-Version N
- Neue Webanwendung wird mit IDB-Version N+1 gepusht
- Nutzer ruft die PWA auf, wodurch der Download eines neuen Dienst-Workers ausgelöst wird
- Der neue Service Worker liest aus der IDB, bevor er den
install
-Ereignis-Handler registriert. Dadurch wird ein IDB-Upgrade-Zyklus von N auf N + 1 ausgelöst. - Da der Nutzer einen alten Client mit Version N hat, hängt der Dienstworker-Upgrade-Vorgang, da noch aktive Verbindungen zur alten Version der Datenbank bestehen.
- 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 nie installiert wurde, erhielten die Nutzer die aktualisierte App nie.
Ausfallsicherheit
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. Jeder langlaufende Prozess sollte jederzeit fortgesetzt werden können.
Bei einem Synchronisierungsprozess, bei dem große Dateien auf den Server hochgeladen und in der IDB gespeichert wurden, haben wir für unterbrochene Teiluploads das fortsetzbare System unserer internen Uploadbibliothek genutzt. Dabei wird die fortsetzbare Upload-URL vor dem Upload in der IDB gespeichert und dann verwendet, um den Upload fortzusetzen, falls er beim ersten Mal nicht abgeschlossen wurde. Außerdem wurde vor jedem langwierigen E/A-Vorgang der Status in der IDB gespeichert, um anzugeben, wo wir uns bei jedem Datensatz im Prozess befanden.
Keine Abhängigkeit vom globalen Zustand
Da Service Worker in einem anderen Kontext existieren, sind viele Symbole, die Sie erwarten würden, 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 usw.). Der Code muss die verwendeten Dienste wie lokalen Speicher oder Cookies schützen. Mit globalThis
können Sie auf das globale Objekt in einer Weise verweisen, die für alle Kontexte funktioniert. Verwenden Sie Daten, die in globalen Variablen gespeichert sind, sparsam, da nicht garantiert werden kann, wann das Script beendet und der Status gelöscht wird.
Lokale Entwicklung
Eine wichtige Komponente von Service Workern ist das lokale Caching von Ressourcen. Während der Entwicklung ist das jedoch genau das Gegenteil dessen, was Sie möchten, insbesondere wenn Updates 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 können Sie dies über die Chrome DevTools erreichen, indem Sie das Kästchen Umgehen für Netzwerk (Bereich Anwendung > Bereich Dienstworker) aktivieren und zusätzlich das Kästchen Cache deaktivieren im Bereich Netzwerk aktivieren, um auch den Arbeitsspeicher-Cache zu deaktivieren. Um mehr Browser abzudecken, haben wir uns für eine andere Lösung entschieden: Wir haben ein Flag hinzugefügt, um das Caching in unserem Service Worker zu deaktivieren, das in Entwicklerbuilds standardmäßig aktiviert ist. 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, die sich für PWAs eignen. Es scannt eine Website und generiert Berichte zu PWAs, Leistung, Barrierefreiheit, SEO und anderen Best Practices. Wir empfehlen, Lighthouse in der kontinuierlichen Integration auszuführen, damit Sie benachrichtigt werden, wenn eines der Kriterien für eine PWA nicht erfüllt ist. Das ist uns tatsächlich einmal passiert, als der Service Worker nicht installiert wurde und wir es erst vor einem Produktionspush bemerkt haben. Mit Lighthouse wäre das verhindert.
Continuous Delivery nutzen
Da Service Worker automatisch aktualisiert werden können, haben Nutzer keine Möglichkeit, Upgrades einzuschränken. Dadurch wird die Anzahl der veralteten Clients erheblich reduziert. Wenn der Nutzer unsere App öffnete, stellte der Service Worker den alten Client bereit, während er den neuen Client träge herunterlud. Nach dem Download des neuen Clients werden Nutzer aufgefordert, die Seite zu aktualisieren, um auf neue Funktionen zuzugreifen. Auch wenn der Nutzer diese Aufforderung ignoriert, erhält er beim nächsten Aktualisieren der Seite die neue Version des Clients. Daher ist es für Nutzer ziemlich schwierig, Updates auf die gleiche Weise abzulehnen wie bei iOS-/Android-Apps.
Wir konnten wichtige Backend-Änderungen mit einer sehr kurzen Migrationszeit für Kunden einführen. Normalerweise geben wir Nutzern einen Monat Zeit, um auf neuere Clients umzustellen, bevor wir bahnbrechende Änderungen vornehmen. Da die App auch dann ausgeliefert wurde, wenn sie veraltet war, war es möglich, dass ältere Clients in der Wildnis existierten, wenn der Nutzer die App lange nicht geöffnet hatte. Auf iOS-Geräten werden Service Worker nach einigen Wochen entfernt, sodass dieser Fall nicht eintritt. Bei Android kann dieses Problem dadurch gemildert werden, dass die Inhalte nicht ausgeliefert werden, wenn sie veraltet sind, oder dass sie nach einigen Wochen manuell ablaufen. In der Praxis sind uns nie Probleme mit veralteten Clients begegnet. Wie streng ein bestimmtes Team hier vorgehen 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, im Kontext eines Dienstarbeiters auf Cookie-Werte zuzugreifen. In unserem Fall mussten wir auf Cookie-Werte zugreifen, um ein Token zu generieren, mit dem API-Anfragen von selbst erhobenen Daten authentifiziert werden konnten. In einem Service Worker sind keine synchronen APIs wie document.cookies
verfügbar. Sie können jederzeit eine Nachricht vom Service Worker an aktive (fensterorientierte) Clients senden, um die Cookie-Werte anzufordern. Es ist jedoch möglich, dass der Service Worker im Hintergrund ausgeführt wird, ohne dass fensterorientierte Clients verfügbar sind, z. B. während 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 Dienst-Worker hat eine Netzwerkanfrage an diesen Endpunkt gesendet und die Antwort gelesen, um die Cookie-Werte 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 bei nicht generierten Service Workern
Sorgen Sie dafür, dass das Service Worker-Script geändert wird, 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. Dienstprogramme werden nur installiert, wenn der Browser erkennt, dass sich das Dienstprogramm-Script geändert hat. Daher mussten wir dafür sorgen, dass sich die Dienstprogramm-Scriptdatei selbst ändert, wenn sich eine im Cache gespeicherte Datei geändert hat. Dazu haben wir manuell einen Hash des Dateisatzes mit den statischen Ressourcen in unser Service Worker-Script eingebettet. So wurde für jedes Release eine separate Service Worker-JavaScript-Datei erstellt. Mit Dienstworker-Bibliotheken wie Workbox lässt sich dieser Prozess automatisieren.
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 sehr mühsam sein, da Sie den Ereignistrigger und das Ereignisobjekt mocken, auf den respondWith()
-Callback warten und dann auf das Promise warten müssen, bevor Sie das Ergebnis prüfen können. Eine einfachere Strukturierung besteht darin, die gesamte Implementierung in eine andere Datei auszulagern, die sich leichter testen lässt.
import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));
Aufgrund der Schwierigkeiten beim Unit-Testen eines Service Worker-Scripts haben wir das Haupt-Service Worker-Script so einfach wie möglich gehalten und den Großteil der Implementierung in andere Module aufgeteilt. Da es sich bei diesen Dateien um Standard-JS-Module handelte, konnten sie mit Standard-Testbibliotheken einfacher als Einheitentests ausgeführt werden.
Teile 2 und 3 folgen demnächst.
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, sehen Sie sich unsere Autorenprofile an, um herauszufinden, wie Sie uns kontaktieren können: