Progressive Web Apps

Was muss ich vorab zu PWA wissen?

Die ursprünglich im Jahr 2015 von Google vorgeschlagene Technologie gewinnt stetig an Bedeutung, da sie leicht zu implementieren ist und einen Mehrwert an User Experience bieten kann. Für Progressive Web Apps wird ein sogenannter Service Worker benötigt, der aktuell nur von Google Chrome und Firefox unter Android unterstützt wird. Die Verbreitung dieser Technologie beträgt in Deutschland momentan knapp 59% und weltweit ca. 74%.

Dennoch kann sich ein Einsatz lohnen, denn immer mehr Browserhersteller arbeiten an der Unterstützung des Service Workers. So hat Apple im Technology Preview von Safari bereits eine partielle Unterstützung implementiert.

Can I Use – Service Workers (Stand: Oktober 2017)

Bevor mit der Entwicklung der Progressive Web App (PWA) begonnen wird, sollte man sich vorab Gedanken zum Zweck und Ziel der Funktion machen. Mit Hilfe des Service Workers können folgende Funktionen realisiert werden:

  • Offline Zugriff auf bereits besuchte Seiten und Darstellung einer Offline-Seite
  • Leistungssteigerung durch das lokale Speichern von Ressourcen
  • Nutzung weiterer browserspezifischer Funktionen, z. B. Push-Benachrichtigungen

Diese Dateien werden zum Starten benötigt:

  • manifest.json Titel und Beschreibung der PWA, Icons, Farben und Art der Browser-Darstellung
  • sw.js Service Worker zur Steuerung der PWA

Weiterhin funktioniert der Service Worker nur über eine verschlüsselte HTTPS-Verbindung. Andernfalls wird von Google Chrome die Fehlermeldung „Uncaught (in promise) DOMException: Only secure origins are allowed.“ ausgegeben.

Wie erstelle ich eine Progressive Web App?

Über die manifest.json werden die Parameter für die Progressive Web App definiert. Darunter der kurze Name (Anzeige auf dem Home-Screen), lange Name (Titel der PWA auf dem Splash Screen) und die Beschreibung. Weiterhin werden die Farben, die Icons und die Start-URL für den Browser gesetzt.

Mit Hilfe des display-Attributes wird definiert, ob die PWA nur als Tab im Browser (minimal-ui oder browser) oder als standalone WebView angezeigt werden soll. Wird die PWA im Vollbildmodus ausgeführt, so können über die CSS Pseudo-Klasse :fullscreen Anpassungen am Frontend vorgenommen werden.

{
    "name": "Sebastian Blum GmbH",
    "short_name": "sblum",
    "description": "Sebastian Blum GmbH aus Zolling in der Nähe von München",
    "lang": "de",
    "start_url": "./",
    "display": "standalone",
    "orientation": "any",
    "background_color": "#fafafa",
    "theme_color": "#9b348e",
    "icons": [
        {
            "src": "icon-64x64.png",
            "sizes": "64x64",
            "type": "image/png"
        },
        { ... }
    ]
}

Nun muss der Service Worker im Browser, falls dieser diese Funktion unterstützt, registriert werden. Dafür wird das HTML-Template um eine Manifest-Definition und einen Skript-Block erweitert.

Der Service Worker durchläuft einen Lifecycle, der über Event-Listener mit Funktionen angepasst werden kann.

<link rel="manifest" href="/manifest.json">
<script>
   if ( "serviceWorker" in navigator ) {
       navigator.serviceWorker.register( "sw.js", {
           scope: "/"
       });
   }
</script>

Phase 1: Install

Ist kein Service Worker registriert, wird dieser nach dem Aufruf installiert. Hier werden alle Seiten definiert, die beim Seitenaufruf automatisch im Cache gespeichert werden.

Weiterhin können über Konstanten die Version, der Cache-Name und die Offline-URL gesetzt werden.

const VERSION = "1.0";
const CACHE_VERSION = "sblum-offline-v" + VERSION;
const OFFLINE_URL = "/offline";

let cacheablePages = [
    "/",
    "/offline",
    "/css/public.css",
    "/fonts/font-awesome/fontawesome-webfont.woff2",
    "/img/header/homepage.jpg",
    "/img/logo/logo.svg",
    "/js/public.js"
];

// Pre-Cache all cacheable pags
self.addEventListener( "install", event => {
   event.waitUntil( () => {
       return caches.open( CACHE_VERSION ).then( cache => {
           return cache.addAll( cacheablePages );
       });
   });
});

Phase 2: Activate

War die Installation erfolgreich, wird der Service Worker aktiviert. Dabei werden alle veralteten Cache-Storages gelöscht. Sollte bereits ein Service Worker installiert und aktiviert sein, werden Phase 1 und 2 übersprungen und es können direkt fetch und message verarbeitet werden.

// Cleanup old cache storages
self.addEventListener( "activate", event => {
   event.waitUntil(
       caches.keys().then( cacheNames => {
           return Promise.all(
               cacheNames.map( cacheName => {
                   if ( CACHE_VERSION !== cacheName ) {
                       return caches.delete( cacheName );
                   }
               })
           );
       })
   );
});

Phase 3: Fetch / Message

Der Service Worker wartet nun im Idle-Status, bis das Fetch-Event ausgelöst wird. In diesem Fall wird geprüft, ob die Response aus dem Cache-Storage bedient werden kann. Ist dies nicht möglich, wird der Request geklont, sein HTTP-Statuscode geprüft und die Antwort im Cache-Storage für zukünftige Anfragen gespeichert.

self.addEventListener( "fetch", event => {
    event.respondWith(
        caches.match( event.request )
            .then( response => {
                // Cache hit » return response
                if ( response ) {
                    return response;
                }

                // Clone the request
                var fetchRequest = event.request.clone();

                return fetch( fetchRequest ).then(
                    response => {
                        // Check response
                        if ( !response || response.status !== 200 || response.type !== 'basic' ) {
                            return response;
                        }

                        // Clone the response
                        var responseToCache = response.clone();

                        caches.open( CACHE_VERSION )
                            .then( cache => {
                                cache.put( event.request, responseToCache );
                            });

                        return response;
                    }
                );
            })
            .catch( () => {
                // Return the offline page
                if ( caches.match( OFFLINE_URL) ) {
                    return caches.match( OFFLINE_URL );
                }
            })
    );
});

Phase 4: Redundant

Der Service Worker wird gestoppt, sobald eines der folgende Ereignisse eintritt:

  • Die Installation des Service Workers schlägt fehl
  • Die Aktivierung des Service Workers schlägt fehl
  • Ein neuer Service Worker ersetzt den bestehenden

Wie kann ich die PWA debuggen und analysieren?

Bei Google Chrome können über die Entwicklertools im Tab „Application“ weitere nützliche Informationen abgerufen werden. Sollte dieser fehlen, kann er über die drei vertikalen Punkte unter dem Menüpunkt „More tools“ hinzugefügt werden.

  • Application » Manifest liefert eine Interpretation der `manifest.json` und zeigt die hinterlegten Farben und Icons.
  • Application » Service Workers zeigt, ob ein Service Worker aktiviert wurde und ob bei der Ausführung Fehler aufgetreten sind. Hier kann zu Entwicklungszwecken auch ein Haken bei „Update on reload“ gesetzt werden, womit bei jedem Seitenaufruf der Service Worker automatisch neu installiert wird. Weiterhin kann der Service Worker über Unregister manuell entfernt werden.
  • Application » Clear storage löscht auf Knopfdruck alle Elemente der aktuellen PWA und kann somit den Browser zurückgesetzen.
  • Cache » Cache Storage zeigt alle gecachten Dateien eines Speichers mit Typ, Dateigröße und Erstellungszeit.

Über die Audits kann die PWA anschließend getestet und auf ihr Optimierungspotential hin geprüft werden. Dort kommt das Open-Source Tool Lighthouse zum Einsatz. Es testet sowohl die grundlegenden Bedienungen (serviceWorker registriert, Auslieferung über HTTPS, usw.) als auch zusätzliche Funktionen (Offline-Seite, Splash Screen, usw.).

Alternativ kann Lighthouse auch über die Kommandozeile über das NPM-Paket GoogleChrome/lighthouse genutzt werden.

Analyse mit Lighthouse