Benutzer:Felix Riesterer/PWA
Dieser Artikel möchte die grundlegendsten Dinge vorstellen, die es beim Erstellen einer Progressive Web App braucht (Stand Sommer 2021).
Inhaltsverzeichnis
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).
HTML-Dokument
Hier der HTML-Quelltext, der die prinzipiell benötigten Anteile enthält:
<!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>
Manifest-Datei
In dieser JSON-Datei sind folgende Angaben für das Projekt wichtig:
{
"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
}
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).
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
}
);
}
}
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.
Die eigentliche App
Es bleibt also nur noch, die Teile zusammenzufügen:
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();
});
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.
Fertige PWA
Hier ist sie nun, unsere Taschenrechner-App: [1]
Web-Links
- Spezifikation der Manifest-Datei auf w3.org
- Artikel zu Progressive Web App bei Wikipedia
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!
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.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.