JavaScript/Tutorials/App/Offline-Browsing

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

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, die auf dem Gerät des Nutzers installiert wird und damit immer verfügbar ist

Das Konzept einer PWA

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

Um jetzt weiterarbeiten zu können, nutzen die Browser nun die im Cache gespeicherten Dateien, Skripte und Bilder. Um hier eine einheitliche Schnittstelle zur Verfügung zu stellen, wird beim ersten Aufrufen der Seite ein Manifest geladen, in dem alle für das Offline-Browsing benötigten Ressourcen wie 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 10.11.2023).

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/nice-highres.png",
      "sizes": "192x192",
      "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. Leider unterstützt Safari (Stand: November 2023) immer noch keine SVG-Favicons, sodass das Start-Icon als große PNG-Rastergrafik gespeichert werden muss.

Mit scope ist der URL-Bereich gemeint, für den der Service Worker zuständig sein soll. Hier wird eine absolute Pfadangabe erwartet.


theme-color

Mit der Angabe einer theme-color ist es möglich, auf mobilen Geräten farbige Adresszeilen und Tabs zu gestalten. In Verbindung mit einem Favicon Ihres Logos in einer größeren Auflösung (192×192px) können Sie Ihre Webseite so unverwechselbar gestalten.[1]

Beispiel
<meta name="theme-color" content="#ff0000"> <link rel="icon" sizes="192x192" href="nice-highres.png"> <!-- iOS Safari --> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
Beachten Sie: Dies funktioniert im Augenblick nur in Chrome auf mobilen Geräten und mit iOS Safari 15.
Bei einer PWA ist diese Festlegung eigentlich nicht nötig, da sie ja im Vollbild-Modus läuft - interessant wäre sie nur während, bzw. vor der Installation.

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.

Fazit

Viele im Netz vorhandene Tutorials zeigen PWAs mit WebSockets. Ein solches Beispiel ist für Hobby-Anwender schwierig zu realisieren, da die meisten managed-Server nur „normales“ HTTP können.

Eine Umsetzung wird hier gezeigt:

Quellen

  1. css-tricks.com: Meta Theme Color and Trickery