PWA bei Google erstellen, Teil 1

Was das Bulletin-Team bei der Entwicklung einer PWA über Dienstprogramme gelernt 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 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 bei Weitem keine vollständige Übersicht über PWAs. Ziel ist es, die Erfahrungen unseres Teams zu teilen.

In diesem ersten Beitrag gehen wir zuerst auf einige Hintergrundinformationen ein und sehen uns dann alles an, 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. Das ist besonders wertvoll, da Bulletin in mehreren Märkten getestet wird.
  • Eine einzige Codebasis Unsere Nutzer waren ungefähr gleichmäßig auf Android- und iOS-Geräte verteilt. Mit einer PWA konnten wir eine einzige Webanwendung erstellen, die auf beiden Plattformen funktioniert. Dadurch konnte das Team schneller arbeiten und mehr erreichen.
  • 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 für die App erforderlich. Bei einer PWA bedeutete das oft einfach, eine URL zu öffnen.
  • 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

Ohne Dienstworker gibt es keine PWA. 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 sie, wenn möglich.

Schreiben Sie ein Service Worker-Script nicht manuell. Wenn Sie Dienstprogramme manuell schreiben, müssen Sie die zwischengespeicherten Ressourcen manuell verwalten und die Logik neu schreiben, die für die meisten Dienstprogrammbibliotheken wie Workbox üblich ist.

Aufgrund unseres internen Technologie-Stacks konnten wir jedoch keine Bibliothek zum Generieren und Verwalten unseres Service Workers verwenden. Das spiegelt sich auch in den Erkenntnissen unten wider. 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.). Alle wichtigen Bibliotheken, die Sie für Ihre Anwendung benötigen, müssen mit Service Workern kompatibel sein. 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:

  1. Der Nutzer hat eine Webanwendung mit IndexedDB-Version N
  2. Neue Webanwendung wird mit IDB-Version N+1 gepusht
  3. Nutzer ruft die PWA auf, wodurch der Download eines neuen Dienst-Workers ausgelöst wird
  4. 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.
  5. Da der Nutzer einen alten Client mit Version N hat, hängt der Upgrade-Vorgang des Dienstarbeiters, da noch aktive Verbindungen zur alten Version der Datenbank geöffnet sind.
  6. Der Dienst-Worker hängt und wird nie installiert

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

Ausfallsicherheit

Service Worker-Scripts werden zwar im Hintergrund ausgeführt, können aber jederzeit beendet werden, auch wenn gerade E/A-Vorgänge (Netzwerk, IDB usw.) ausgeführt werden. 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.

Nicht vom globalen Zustand abhängig sein

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 wollen, insbesondere wenn Updates verzögert erfolgen. Sie möchten den Server-Worker jedoch weiterhin installiert lassen, damit Sie Probleme damit beheben oder mit anderen APIs wie der Hintergrundsynchronisierung oder Benachrichtigungen arbeiten 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. Es ist wichtig, auch den Cache-Control: no-cache-Header einzufügen, um das Caching von Assets im Browser zu verhindern.

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 als Teil unserer CI wäre das nicht passiert.

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-Geräten lässt sich dieses Problem vermeiden, indem die Auslieferung nicht erfolgt, wenn die Inhalte veraltet sind, oder indem die Inhalte 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. Um dieses Problem zu umgehen, haben wir einen Endpunkt auf unserem Frontend-Server erstellt, der den Cookie-Wert einfach an den Client zurückgab. 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 Umgehung für Browser, die sie unterstützen, nicht mehr erforderlich sein, da sie asynchronen Zugriff auf Browser-Cookies bietet und direkt vom Dienst-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 besteht darin, dass ein Service Worker alle statischen Anwendungsdateien während der install-Phase installiert. So können Clients bei allen nachfolgenden Besuchen direkt auf den Cache der Cache Storage API zugreifen. 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 Service Worker-Kernscript 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: