Benutzer:Felix Riesterer/PWA

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Dieser Artikel möchte die grundlegendsten Dinge vorstellen, die es beim Erstellen einer Progressive Web App braucht (Stand Sommer 2021).

Das Konzept einer PWA

Eine PWA ist im Grunde nichts anderes als eine gewöhnliche Internetseite, welche von einem Browser regulär angezeigt werden kann. Durch ein paar Extras „versteht“ der Browser jedoch, dass er für diese Seite ein besonderes Lesezeichen anlegen kann, um sie offline verfügbar zu machen. Damit eignet sich eine PWA als plattformübergreifende Mobil-App.

Service Worker

Eine Mobil-App spielt aber erst dann ihren vollen Wert aus, wenn sie den für sie notwendigen Dienst im Internet erreichen kann. Deshalb braucht es auf der einen Seite Funktionalität, die offline ohne diesen Dienst auskommt und die App prinzipiell funktionstüchtig macht, auf der anderen Seite aber eine weitere Funktionalität, die den Dienst im Internet in Anspruch nimmt. Um diesem Widerspruch gerecht zu werden (offline/online) unterstützen die wichtigen Browser (und vor allem die mobilen Browser) sogenannte Service Worker, die im Hintergrund das Laden von Daten oder Dateien regeln, damit man sich bei seiner Programmlogik in der App nur noch um Erfolg und Scheitern eines Ladevorgangs kümmern muss. Wer also eine PWA erstellt, benötigt eine JavaScript-Datei eigens für den Service Worker, in der die notwendige Logik enthalten ist, um offline und online miteinander in Einklang zu bringen.

Manifest

Damit das Lesezeichen und der Startvorgang der App einigermaßen standardisiert vorgeschrieben werden kann, benötigt eine PWA eine Manifest-Datei, in der unter Anderem der Titel der App, der Pfad zum HTML-Dokument für die Einstiegsseite der App, Pfade zu den Icon-Bilddateien (und ihre jeweiligen Dateiformate) oder auch eine Hintergrundfarbe festgelegt werden. Diese JSON-Datei wird im HTML-Dokument über ein link-Element referenziert.

Taschenrechner-App

Als Beispiel-App verwendet dieser Artikel einen simplen Taschenrechner, wie er im Taschenrechner-Tutorial vorgestellt wird (Version vom 23.07.2021).

Beachten Sie: Im Grunde bräuchte unsere Taschenrechner-App die ganzen Zutaten einer PWA nicht, denn es gibt für einen Taschenrechner keinen Grund irgendwelchen Datenaustausch mit einem Server zu betreiben, oder Dateien laden zu müssen. Um aber auf Mobilgeräten die Installation der App vorgeschlagen zu bekommen, müssen wir hier so tun, als bräuchte unser Taschenrechner all das, damit er als eine PWA anerkannt wird und das Smartphone oder Tablet die Installation auch wirklich anbietet.

HTML-Dokument

Hier der HTML-Quelltext, der die prinzipiell benötigten Anteile enthält:

Grundgerüst
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Taschenrechner</title>
    <link rel="manifest" href="./manifest.json">
    <meta name="theme-color" content="#2a3443" />
    <!-- iOS -->
    <link rel="apple-touch-icon" href="./images/icon-144x144.png" />
    <meta name="mobile-web-app-capable" content="yes" />
    <meta name="mobile-web-app-status-bar-style" content="black" />
    <meta name="mobile-web-app-title" content="SELFHTML-Taschenrechner" />
    <!-- /iOS -->
    <style>
    </style>
    <script>
    </script>
  </head>
  <body>
  </body>
</html>
Die Spezifikation setzt zwingend den HTML5-Dokumenttyp voraus. Danach folgt eine Angabe zur verwendeten Textkodierung (bitte weichen Sie nur in gut begründeten Ausnahmefällen von UTF-8 ab!) und der Titel des Dokuments.

Der Titel im HTML-Dokument spielt nur dann eine Rolle, wenn jemand das Dokument über den Browser im regulären Modus aufruft, also die Seite über einen Link oder Eingabe der Adresse öffnet. Die Beschriftung des Icons in der Liste der auf dem Smartphone installierten Apps wird nicht über den Inhalt des title-Elements geregelt!
Die für diese PWA geltende Manifest-Datei wird wie oben bereits beschrieben mit einem link-Element referenziert.
Ältere Versionen von iOS respektieren eventuell die Manifest-Datei nicht, weshalb manches aus der Manifest-Datei in Form von meta-Elementen kompensiert werden muss. Diese stehen zwischen den beiden HTML-Kommentaren.

Man kann bei komplexeren App-Projekten CSS- und JavaScript-Code selbstverständlich in externe Dateien auslagern, für dieses kleine Beispielprojekt wird beides direkt im HTML-Dokument notiert werden.

Manifest-Datei

In dieser JSON-Datei sind folgende Angaben für das Projekt wichtig:

manifest.json
{
  "name": "SELFHTML-Taschenrechner",
  "short_name": "SelfCalc",
  "theme_color": "#2a3443",
  "background_color": "#2a3443",
  "display": "standalone",
  "orientation": "portrait",
  "serviceworker": {
    "src": "worker.js",
    "scope": "/self/SELFHTML-PWA"
  },
  "start_url": "pwa.html",
  "icons": [
    {
      "src": "images/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    ...
  ],
  "splash_pages": null
}
Die Eigenschaft start_url bezeichnet das HTML-Dokument, welches als Einstieg in die App aufgerufen werden soll. Die Liste der Icons ist hier verkürzt dargestellt, enthält aber verschiedenste Bilddateien für unterschiedliche Auflösungen.
Mit scope ist der URL-Bereich gemeint, für den der Service Worker zuständig sein soll. Hier wird eine absolute Pfadangabe erwartet.

Service Worker

Dieses JavaScript-Programm soll nun regeln, wie die angeforderten Daten oder Dateien aus dem Internet oder dem Browser-Cache geladen werden sollen. Dazu gibt es prinzipiell zwei Prinzipien:

1. Internet zuerst (web first) 2. Cache zuerst (cache first)

Für unsere Zwecke benötigen wir eigentlich überhaupt keine Daten aus dem Internet, da wir ja nur einen Taschenrechner umsetzen wollen. Jedoch wird uns das Installieren der PWA nicht automatisch (im Standardbrowser unter Android) angeboten werden, wenn wir keinen funktionierenden Service Worker haben, dessen Code tatsächlich dazu geeignet ist, seine Rolle zu spielen. Außerdem hat es einen Sinn, wenn wir im Service Worker alle benötigten Dateien (HTML, Manifest, Icons) angeben, damit er diese in den Cache lädt, um unsere App vollständig offline verfügbar zu machen.

Der hier vorgestellte Service Worker ist praxiserprobt und kann mehr, als für dieses Mini-Projekt benötigt wird. Er ist auf cache first ausgelegt. Will man gezielt web first haben, so benötigt man entweder einen passenden Cache-Control-Header mit dem Wert no-cache, oder man verwendet POST als HTTP-Verb (wie z.B. beim Versand von Daten).

worker.js
const cacheName = "SelfhtmlCalculator";

const staticAssets = [
  "./pwa.html",
  "./manifest.json",
  "./images/icon-72x72.png"
];

self.addEventListener("install", async (event) => {
  const cache = await caches.open(cacheName);
  cache.addAll(staticAssets);
});

self.addEventListener("activate", (event) => {
  clients.claim();
});

self.addEventListener("fetch", (event) => {
  const req = event.request;
  const noCache = (
    req.headers.get("Cache-Control") == "no-cache"
    || req.method.match(/^post/i)
  );

  return event.respondWith(
    noCache
    ? fromNet(req)
    : fromCache(req)
  );
});

async function fromCache (req) {
  const cache = await caches.open(cacheName);
  return await cache.match(req) || await fromNet (req);
}

async function fromNet (req) {
  const cache = await caches.open(cacheName);

  try {

    const fresh = await fetch(req);

    if (!req.method.match(/^post$/i)) {
      cache.put(req, fresh.clone());
    }

    return fresh;

  } catch (e) {

    return new Response(
      "Not found.",
      {
        headers: { "Content-Type": "plain/text" },
        status: 404
      }
    );
  }
}
Jeder Service Worker verwendet seinen eigenen Browser-Cache, der durch einen eindeutigen Namen definiert wird. Wir verwenden „SelfhtmlCalculator“.

Die Liste der im Cache unbedingt für den Offlinebetrieb vorzuhaltenden Dateien wird in der Variable staticAssets definiert.
Das install-Ereignis wird bei der ersten Verwendung des Service Workers ausgelöst. Hier sorgt eine passende EventListener-Funktion mit cache.addAll() dafür, dass alle unsere für den Offline-Betrieb benötigten Dateien in den Cache geladen werden.
Das activate-Ereignis wird ausgelöst, wenn unsere App den Service Worker für sich in Anspruch nimmt. Unser Service Worker sorgt in dem Moment dafür, dass alle Anfragen unserer App ins Internet ab sofort über ihn laufen.

Das eigentliche Laden von Daten erledigt das fetch-Ereignis, bei dem in diesem Programm geprüft wird, ob es unbedingt aus dem Internet laden soll, oder ob die Daten im Browser-Cache genügen.

Die eigentliche App

Es bleibt also nur noch, die Teile zusammenzufügen:

App-Logik
class SelfhtmlCalculator {

  constructor () {
    if (!this.registerServiceWorker()) {

      const p = document.querySelector("main").appendChild(
        document.createElement("p")
      );

      p.innerHTML = (
        "de" == navigator.language.substr(0, 2)
        ? "Ihr Browser unterstützt anscheinend keine Service-Worker. Daher kann die App nicht installiert werden."
        : "Your browser doesn't seems to support service workers. Therefore this app can't be installed."
      );

      p.classList.add("error");
    }

    this.calculator();
  }

  calculator () {
    // https://wiki.selfhtml.org/wiki/JavaScript/Tutorials/Taschenrechner
  }

  async registerServiceWorker () {
    let ok = ("serviceWorker" in navigator);

    if (ok) {

      try {

        await navigator.serviceWorker.register("./worker.js");

      } catch (e) {

        ok = false;
      }
    }

    return ok;
  }
}

document.addEventListener("load", () => {
  new SelfhtmlCalculator();
});
Wir notieren unsere App als eine Klasse SelfhtmlCalculator, die wir nach dem Laden des HTML-Dokuments (Ereignis load) mit dem Schlüsselwort new instanziieren.

Wenn von einer Klasse ein Objekt erstellt wird, werden die Anweisungen in der zugehörigen Konstruktorfunktion (in JavaScript immer mit dem dafür reservierten Namen constructor) ausgeführt. Hier wird zuerst der Service Worker über die Methode registerServiceWorker eingerichtet und dann der Taschenrechner gestartet.
Scheitert das Einrichten des Service Workers, wird ein entsprechender Hinweis ins Dokument geschrieben.

In der Methode registerServiceWorker wird mit dem Schlüsselwort await auf ein Ergebnis gewartet. Um dieses Schlüsselwort und das Warten so nutzen zu können, muss bei der Definition dieser Funktion vor den Funktionsnamen das Schlüsselwort async (steht für asynchrone Verarbeitung) notiert sein.


Fertige PWA

Hier ist sie nun, unsere Taschenrechner-App: [1]

Web-Links