Aggiornamenti automatici delle applicazioni Angular con service worker

Nel lungo post precedente abbiamo visto come fare per spacchettare in due un monolite e rendere autonoma la single page application, sviluppata con Angular 8, che costituisce il frontend del progetto su cui lavoro.

Rendere del tutto indipendente l’applicazione frontend da quella backend, però, ha un effetto collaterale piuttosto interessante: una volta che hai una single page application servita come un set di file HTML/CSS/JS statici, nulla vieta agli utenti di aprire l’applicazione una volta nella vita, tenere aperto il tab del browser per sempre e non aggiornarla mai.

Noi però sull’applicazione continuiamo a lavorare, a sistemare bug e ad aggiungere feature, e ho già assistito troppe volte a una conversazione di questo tipo:

“Non funziona”
“Refresha”
“Ah ok, adesso va”

Sarebbe bello, quindi, che il browser sapesse quando è stata rilasciata una versione nuova dell’applicazione e si aggiornasse.

La mia prima soluzione era una cosa assolutamente involuta, con un endpoint dell’applicazione frontend che espone la variabile di ambiente di Heroku con lo SHA-1 dell’ultimo commit, e l’applicazione stessa che forza un window.location.reload() quando il valore ritornato è diverso da quello che ha salvato nel sessionStorage, ma poi mi sono chiesto se non ci fosse qualcosa di più semplice, dato che non mi pare esattamente un problema solo mio.

La risposta, infatti, è che esiste un modo praticamente gratis per farlo, con una tecnologia che piace ai giovani e che è estremamente semplice da usare: i service worker.

Angular, nello specifico, ha un service worker suo che fa esattamente questo: tiene in cache una lista di file e asset statici che gli diciamo noi in un file di configurazione ed espone un servizio che triggera degli eventi quando questi vengono aggiornati.

Inizia tutto così: ng add @angular/pwa.

Così facendo, la nostra applicazione diventa magicamente una PWA che installa un service worker sui browser degli utenti; è tutto magicamente già configurato e justworka, serve solo dare un’occhiata alla lista dei file cachati e autoaggiornati nel file ngsw-config.json e verificare che ci siano tutti i file che compongono l’applicazione (tipicamente /*.js e /*.css sono sufficienti)

Ok, ora ho un service worker, cosa ci faccio?

Posso registrare un servizio che polli il server per aggiornamenti ai file che il service worker tiene in cache, e quando c’è un aggiornamento forzare un refresh del browser, per l’appunto:

@Injectable()
export class AutoUpdaterService {

  constructor(appRef: ApplicationRef, updates: SwUpdate) {
    console.log('Subscribing to application updates for autorefresh...');
    updates.available.subscribe(event => {
        console.log('Application was updated, need to refresh!');
        updates.activateUpdate().then(() => document.location.reload());
    });

    console.log('Configuring application update polling');
    const appIsStable$ = appRef.isStable.pipe(first(isStable => isStable === true));
    const everyMinute$ = interval(60 * 1000);
    const everyMinuteOnceAppIsStable$ = concat(appIsStable$, everyMinute$);

    everyMinuteOnceAppIsStable$.subscribe(() => {
      updates.checkForUpdate();
    });
  }
}

Ci sono un paio di gotcha non banali: per esempio, cos’è quell’appIsStable$?

I service worker hanno un lifecycle che si articola in diverse fasi, e l’applicazione è definita “stable” solo una volta che il service worker è installato e running; prima di questo momento, fare polling per aggiornamenti sarebbe dannoso, perché si rischia di finire in un loop in cui l’applicazione sembra aggiornata e la riaggiorno prima che abbia finito di installarsi.

Così facendo, quindi, ogni sessanta secondi l’applicazione chiede al service worker se ci sono aggiornamenti nei file che tiene in cache, e in caso ce ne siano forza un refresh del browser: ora, io posso fare questa cosa perché la mia applicazione è fondamentalmente stateless e refresharla “sotto il culo” agli utenti non è un grosso problema, ma nulla vi vieta di mostrare un avviso che dice “ci sono aggiornamenti, per favore refresha”.

Insomma, non c’è davvero bisogno di mettere in piedi cose complicate con gli SHA-1 dei commit: basta usare la tecnologia del web del 2019.