Taschenlampe – PWA-Dokumentation
Diese README fasst alle technischen Hintergründe zusammen, die beim Bau dieser Bildschirm-Taschenlampen-App als installierbare Web-App (PWA) relevant waren.
Inhalt des Projekts
taschenlampe-php/
├── index.php Landingpage mit Erklärung, Install-Link, Anleitung
├── download.php Liefert taschenlampe.html mit erzwungenem Download-Header aus
├── taschenlampe.html Die eigentliche App (einzige "Quelle der Wahrheit")
├── manifest.json Beschreibt die App gegenüber Android/Chrome (Name, Icons, Modus)
├── sw.js Service Worker – regelt Caching & Offline-Fähigkeit
└── icons/
├── icon-192.png
└── icon-512.png
Alle Dateien müssen zusammen im selben Ordner auf einem Webserver liegen und
über HTTPS erreichbar sein (Service Worker funktionieren aus
Sicherheitsgründen nicht über einfaches HTTP, nur über HTTPS oder localhost
beim lokalen Testen).
1. Was ist eine PWA (Progressive Web App)?
Eine PWA ist eine ganz normale Website, die zusätzlich drei Dinge mitbringt:
- Ein Manifest (
manifest.json) – teilt dem Browser mit, dass die Seite "installierbar" ist, inkl. Name, Icons, Startfarbe und Anzeigemodus. - Einen Service Worker (
sw.js) – ein Hintergrund-Skript, das Anfragen abfangen und aus einem lokalen Cache beantworten kann, auch offline. - HTTPS – Pflicht, damit ein Service Worker überhaupt registriert werden darf.
Sind alle drei Voraussetzungen erfüllt, bietet Chrome von sich aus eine "Installieren"-Option an (per Mini-Infoleiste oder – bei uns – per eigenem Button, siehe Abschnitt 5).
Warum reicht die einfache taschenlampe.html allein nicht?
Ohne Service Worker fragt der Browser bei jedem Öffnen der App erneut "Ist die Originaldatei noch erreichbar?". Ist gerade kein Internet da, schlägt das fehl. Mit Service Worker beantwortet der Browser diese Frage stattdessen aus dem lokalen Cache, ganz ohne das Netzwerk zu kontaktieren.
2. Warum file://-Links auf Android nicht (mehr) zuverlässig funktionieren
Ursprünglich lag nahe, die App einfach als lokale Datei zu speichern und per Dateimanager zu öffnen. Das scheitert auf modernem Android aus zwei Gründen:
- Android selbst verbietet Apps seit Version 7.0, rohe
file://-Pfade an andere Apps weiterzugeben (FileUriExposedException). Der Dateimanager reicht Dateien stattdessen über einencontent://-URI weiter (viaFileProvider) – eine Art kontrollierter Vermittler, der den echten Speicherort verbirgt. - Chrome selbst behandelt manuell in die Adresszeile getippte
file://-Adressen bewusst nicht als Navigation, sondern schickt sie als Google-Suche los. Das ist eine gezielte Sicherheits-/Datenschutz- Entscheidung, um zu verhindern, dass präparierte Links Nutzer zum Öffnen lokaler Dateien verleiten.
Konsequenz: Eine über file:///content:// erzeugte
"Zum Startbildschirm hinzufügen"-Verknüpfung ist fragil und funktioniert
offline oft nicht zuverlässig. Der robuste Weg ist stattdessen, die App über
eine echte https://-Adresse zu hosten und per Service Worker cachen zu
lassen (siehe Abschnitt 1) – dann ist es egal, ob beim Öffnen Internet
verfügbar ist oder nicht, weil nichts mehr nachgeladen werden muss.
Derselbe Grundsatz gilt übrigens auch für interne Browser-Seiten wie
chrome://serviceworker-internals oder Firefox' about:serviceworkers: Sie
lassen sich nicht von einer Webseite aus anklickbar verlinken, sondern nur
manuell in die Adresszeile eintippen – ebenfalls aus Sicherheitsgründen
(Schutz vor Clickjacking auf interne Browserfunktionen).
3. Aufbau des Service Workers (sw.js)
const CACHE_NAME = 'taschenlampe-cache-v1';
const APP_SHELL = [
'taschenlampe.html',
'manifest.json',
'icons/icon-192.png',
'icons/icon-512.png'
];
self.addEventListener('install', (event) => { ... }); // 1
self.addEventListener('activate', (event) => { ... }); // 2
self.addEventListener('fetch', (event) => { ... }); // 3
Ein Service Worker durchläuft drei Phasen:
install– wird einmalig beim ersten Besuch ausgelöst. Hier lädtcaches.open(CACHE_NAME).then(cache => cache.addAll(APP_SHELL))alle inAPP_SHELLgelisteten Dateien herunter und legt sie im Cache ab.self.skipWaiting()sorgt dafür, dass ein neuer Worker nicht wartet, bis alle alten Tabs geschlossen sind, sondern sofort aktiv werden will.activate– räumt alte Cache-Versionen auf (jede, deren Name nicht mehrCACHE_NAMEentspricht, wird gelöscht).self.clients.claim()übernimmt sofort die Kontrolle über bereits offene Seiten.fetch– wird bei jeder Netzwerk-Anfrage der Seite ausgelöst (Cache-First-Strategie): Erst wird geschaut, ob eine passende Antwort im Cache liegt (caches.match) – falls ja, wird sie direkt zurückgegeben, ganz ohne Netzwerkzugriff. Nur wenn nichts im Cache liegt, wird das Netzwerk überhaupt kontaktiert.
4. Wo und wie lange werden die gecachten Dateien gespeichert?
- Wo: Im sogenannten Cache Storage des Browsers – einem eigenen
Speicherbereich pro Website/Domain, getrennt vom normalen Browser-
Verlauf/-Cache. Auf Android liegt das technisch irgendwo im internen
App-Speicher von Chrome, ist aber nicht direkt als Datei zugänglich; der
Zugriff läuft ausschließlich über die JavaScript-
caches-API bzw. die DevTools. - Wie lange: Zeitlich unbegrenzt, solange
- der Nutzer den Speicher nicht manuell löscht (Chrome-Einstellungen → Website-Einstellungen → Speicher → betreffende Domain → Löschen),
- die App/den Cache nicht selbst per Code entfernt (z. B. über die
Versionierung in
activate, die alte Caches automatisch löscht), - und Chrome nicht unter Speicherdruck automatisch aufräumt: Bei sehr knappem Gerätespeicher darf der Browser least-recently-used-Caches selbstständig entfernen, um Platz zu schaffen. Das betrifft eher selten genutzte Seiten zuerst, ist aber nie hundertprozentig garantiert.
- Wie viel Platz: Für unsere App verschwindend gering – die gesamte App-Shell (HTML, Manifest, zwei kleine PNGs) liegt im niedrigen Kilobyte-Bereich, weit unter jedem praktischen Limit.
Überprüfen kannst du das über:
- Chrome auf dem Handy:
chrome://serviceworker-internals(Liste aller registrierten Worker mit Status/Scope) - Am PC über USB-Debugging:
chrome://inspect→ inspect → Tab Application → Service Workers / Cache Storage (zeigt exakten Cache-Inhalt) - Praxistest: Flugmodus an, App über Icon öffnen – lädt sie normal, war das Caching erfolgreich
5. Versionierung – warum Änderungen sonst nicht ankommen
Der Service Worker vergleicht nicht automatisch, ob sich
taschenlampe.html inhaltlich geändert hat. Er erkennt Updates nur daran,
dass sich die Datei sw.js selbst (byteweise) verändert hat.
Deshalb bei jeder Änderung an App-Dateien:
const CACHE_NAME = 'taschenlampe-cache-v1'; // → hochzählen auf v2, v3, ...
Ablauf danach:
- Chrome bemerkt beim nächsten Seitenaufruf die geänderte
sw.js - Installiert den neuen Worker im Hintergrund (
install-Event, neuer Cache) skipWaiting()+clients.claim()sorgen dafür, dass der neue Worker sofort übernimmt, statt auf das Schließen aller Tabs zu warten- Alte Cache-Version wird im
activate-Event automatisch gelöscht
Trotzdem am zuverlässigsten zum Testen: App auf dem Handy komplett
schließen (aus der App-Übersicht wegwischen, nicht nur "zurück"), dann erneut
über das Icon öffnen. Alternativ den alten Worker in
chrome://serviceworker-internals manuell per "Unregister" entfernen.
6. Eigener Install-Button statt automatischem Chrome-Dialog
Ohne eigenes Zutun entscheidet Chrome selbst, wann es den Installations-Dialog zeigt (z. B. nach erneutem Besuch einer Seite) – das kann an ungünstigen Momenten passieren. Der Code fängt das ab:
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault(); // automatischen Dialog unterdrücken
deferredInstallPrompt = event; // Event für später merken
installBtn.classList.add('visible'); // eigenen Button einblenden
});
installBtn.addEventListener('click', async () => {
deferredInstallPrompt.prompt(); // jetzt erst den echten Dialog zeigen
await deferredInstallPrompt.userChoice;
});
Wichtig: beforeinstallprompt feuert nur in Chromium-Browsern (Chrome,
Edge, Samsung Internet). Firefox unterstützt dieses Event nicht – dort bleibt
nur der manuelle Weg über "Zum Startbildschirm hinzufügen" im Menü.
7. Firefox-Besonderheit: Style des Helligkeitsreglers
Firefox und Chrome/Edge nutzen unterschiedliche, browserspezifische
CSS-Pseudo-Elemente, um <input type="range"> individuell zu gestalten:
| Zweck | Chrome/Edge (WebKit/Blink) | Firefox |
|---|---|---|
| Native Optik abschalten | -webkit-appearance: none |
-moz-appearance: none |
| Lauffläche/Skala stylen | ::-webkit-slider-runnable-track |
::-moz-range-track |
| Schiebe-Punkt stylen | ::-webkit-slider-thumb |
::-moz-range-thumb |
Der aktuelle Code enthält nur die WebKit-Varianten. Firefox ignoriert diese
komplett, wendet aber background: transparent (das für alle Browser
gilt) auf die native Lauffläche an – dadurch wird die Skala unsichtbar,
während der Schiebe-Punkt in Firefox' Standardoptik weiter angezeigt wird.
Fix: Die drei -moz--Pendants ergänzen (nicht ersetzen), mit denselben
Maßen/Farben wie die bestehenden WebKit-Regeln. Zusätzlich meist
border: none; bzw. background-clip: padding-box; beim Thumb nötig, um
Firefox' eigenen Fokusring/Rahmen zu neutralisieren.
8. Diagnose-Werkzeuge im Überblick
| Werkzeug | Wofür |
|---|---|
chrome://serviceworker-internals (Chrome, Handy) |
Liste aller registrierten Service Worker, Status, manuelles Unregister/Update |
about:serviceworkers (Firefox, Handy) |
Firefox-Pendant dazu |
chrome://inspect (PC, per USB-Debugging) |
Volle DevTools für die mobile Seite – Application-Tab zeigt Service Worker und exakten Cache-Inhalt |
| Chrome-Einstellungen → Website-Einstellungen → Speicher | Cache-Größe pro Domain einsehen, manuell löschen |
| Flugmodus-Test | Einfachster Praxistest, ob Offline-Fähigkeit tatsächlich funktioniert |
unterrichtsportal.org