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:

  1. Ein Manifest (manifest.json) – teilt dem Browser mit, dass die Seite "installierbar" ist, inkl. Name, Icons, Startfarbe und Anzeigemodus.
  2. Einen Service Worker (sw.js) – ein Hintergrund-Skript, das Anfragen abfangen und aus einem lokalen Cache beantworten kann, auch offline.
  3. 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 einen content://-URI weiter (via FileProvider) – 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:

  1. install – wird einmalig beim ersten Besuch ausgelöst. Hier lädt caches.open(CACHE_NAME).then(cache => cache.addAll(APP_SHELL)) alle in APP_SHELL gelisteten 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.
  2. activate – räumt alte Cache-Versionen auf (jede, deren Name nicht mehr CACHE_NAME entspricht, wird gelöscht). self.clients.claim() übernimmt sofort die Kontrolle über bereits offene Seiten.
  3. 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://inspectinspect → Tab ApplicationService 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:

  1. Chrome bemerkt beim nächsten Seitenaufruf die geänderte sw.js
  2. Installiert den neuen Worker im Hintergrund (install-Event, neuer Cache)
  3. skipWaiting() + clients.claim() sorgen dafür, dass der neue Worker sofort übernimmt, statt auf das Schließen aller Tabs zu warten
  4. 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