ist unangenehm: Da hat das Entwicklungsteam die von den Benutzer*innen sehnlich erwartete neue Version der Web-Applikation publiziert, all die neuen Funktionalitäten eingebaut, den REST-Server und die Single-Page-Applikation (SPA) von Grund auf überarbeitet und fein aufeinander abgestimmt, eingehend getestet, zum Zeitpunkt Null den Hebel umgelegt. Und dann kommt der Anruf: Die Anwendung reagiert nicht mehr.
Und man fragt sich: Hat man etwas vergessen, beim Installieren einen Fehler gemacht? Man öffnet die Web-Site im eigenen Browser und: Alles neu, alles da, alles geht. Nur nicht beim Anrufer.
erschliesst sich nach einigen Abklärungen. Auf Nachfrage erklärt der Benutzer, dass er die Applikation schon längere Zeit im Browser geöffnet hatte, schon vor dem Zeitpunkt des Updates.
Das Problem dabei: Eine HTML-Seite wird so lange in der ursprünglich geladenen Version angezeigt, bis man sie verlässt oder explizit (über den Reload-Knopf) neu lädt. Das gilt auch – und ganz besonders – für eine Single-Page-Applikation, die nichts anderes ist als eine einzelne HTML-Seite mit (referenziertem) JavaScript-Code. Für den Benutzer baut sich zwar das Bild scheinbar bei jedem Sprung von Modul zu Modul, von Ansicht zu Ansicht wieder neu auf. In Wahrheit aber zeigt der Browser die ganze Zeit über die gleiche HTML-Seite an. Das Heimtückische: Selbst wenn man den Browser beendet und beim nächsten Start die zuletzt geöffneten Tabs automatisch wiederherstellen lässt, erhält man wieder die selbe alte Applikation aus dem Browser-Cache. Bei einer Web-Applikation, die rege genutzt wird und ständig geöffnet bleibt, kann es auf diese Weise sehr lange dauern, bis die neue Version bei allen Benutzer*innen angekommen ist. Und in der Zwischenzeit können viele Hilferufe bei der Supportnummer eingehen, wenn die alte Version mit den neuen REST-Schnittstellen im Backend nicht klarkommt (was natürlich wenn immer möglich vermieden werden sollte).
ist einfach: Man bittet den Benutzer am Telefon, die Seite mit Reload neu zu laden. Oder man schreibt vorgängig eine E-Mail an alle. Oder zeigt ein Banner in der Applikation. «Update um 10 Uhr, bitte danach mit Reload (F5) nachladen.» Alles gut.
Schöner jedoch wäre es, wenn die Applikation selbst merken würde, wenn sie sich durch ein Update ersetzen sollte.
könnte folgendermassen aussehen: Die Single-Page-Applikation soll sich beim Start merken, welchen Hash-Wert die laufende Version der Applikation besitzt. Danach prüft sie in regelmässigen Abständen, ob auf dem Server noch dieselbe Version zu finden ist. Falls nicht, lädt sie sich neu.
Wir können uns dabei Folgendes zunutze machen:
Wir haben also Glück: Server-seitig braucht es wohl keine Anpassung.
Auf der Client-Seite benötigen wir dagegen einen neuen Dienst, der (unter Umgehung des Browser-Caches) regelmässig den ETag (Hash-Wert) der Datei index.html vom Server abfragt und prüft, ob dieser vom letzten ETag abweicht. Dabei könnte es passieren, dass der Browser die Applikation beim initialen Laden aus seinem Cache gelesen hat, während auf dem Server schon eine neue Version sitzt. Um sicherzustellen, dass uns auch so ein Fall nicht durch die Lappen geht, schreiben wir den zuletzt gelesenen ETag nicht nur in den Arbeitsspeicher, sondern in den Local Storage und führen die Prüfung schon beim Start ein erstes Mal durch.
In Angular könnte eine Umsetzung dieser Idee folgendermassen aussehen:
import {Injectable} from '@angular/core'; import {Observable, timer} from 'rxjs'; import {HttpClient, HttpEventType, HttpHeaders, HttpResponse} from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class AppUpdateCheckerService { // Check for an update initially and then every minute private updateCheckTimer: Observable<number> = timer(0, 60 * 1000); constructor(private http: HttpClient) { } startPeriodicCheck(): void { this.updateCheckTimer.subscribe(counter => this.checkAndReloadAppIfUpdated()); } private checkAndReloadAppIfUpdated(): void { console.debug('Checking for updated index.html'); // Get the header information of the current index.html file on // the server (bypassing the browser cache) this.http.head( document.location.origin, { headers: new HttpHeaders({ 'Cache-Control': 'max-age=0' }), observe: 'events', responseType: 'arraybuffer', }) .subscribe( event => { if (event.type === HttpEventType.Response) { this.handleIndexPageHeaderResponse(event); } }); } private handleIndexPageHeaderResponse( httpResponse: HttpResponse<any>) { if (!httpResponse.ok) { // Ignore failed HTTP responses (the network connection may be // interrupted, the server may be down for an update etc.) return; } const previousIndexPageEtag = localStorage.getItem('AppHash'); const latestIndexPageEtag = httpResponse.headers.get('etag'); // Reload the website if the ETag of index.html is different or // has not yet been stored to localStorage. // This ensures that the latest version of the application is // loaded (from the server rather than the browser cache). // This even works in private browser mode where local storage // is initially empty. // Disadvantage: loading the application for the first time or // in private mode takes slightly longer due to the refresh. if (previousIndexPageEtag !== latestIndexPageEtag) { console.info( `Found new version of index.html` + ` (new ETag: ${latestIndexPageEtag},` + ` previous ETag: ${previousIndexPageEtag})` + ` - reloading web site`); localStorage.setItem('AppHash', latestIndexPageEtag); window.location.reload(); } } }
Um den periodischen AppUpdateCheckerService zu aktivieren, können wir im AppComponent (z.B. im Konstruktor) folgenden Aufruf einfügen:
appUpdateCheckerService.startPeriodicCheck();
Das Ganze ist absichtlich einfach gehalten – in der Praxis könnte man dem Benutzer vor dem Reload vielleicht noch eine Warnung anzeigen. Aber grundsätzlich wär’s das! Nun können wir hoffen und zuversichtlich sein, dass künftige Updates der Single-Page-Applikation ohne Verzug ihren Weg auf alle Browser finden werden.
Nachtrag 08.11.2022:
Das beschriebene Vorgehen funktioniert leider mit vielen Web-Servern nicht sauber. Web-Server können nach eigenem Ermessen einmal eine komprimierte Version und dann wieder eine unkomprimierte Version einer Website zurückgeben. Da die beiden Versionen der Seite verschiedene ETags haben, kommt es dadurch zu ständigem Wechsel der ETags. Sicherer ist es, den Hash von Index.html zu berechnen und diesen anstelle des ETags zu vergleichen, um Updates zu erkennen.
Schreiben Sie einen Kommentar