La vita di un service worker

È difficile capire cosa fanno i Service worker senza comprenderne il ciclo di vita. Il loro funzionamento interno sembrerà opaco, persino arbitrario. È utile ricordare che, come qualsiasi altra API del browser, i comportamenti dei service worker sono ben definiti, specificati e rendono possibili le applicazioni offline, agevolando al contempo gli aggiornamenti senza interrompere l'esperienza utente.

Prima di passare a Workbox, è importante comprendere il ciclo di vita dei Service worker in modo che ciò che Workbox faccia effettivamente.

Definizione dei termini

Prima di iniziare il ciclo di vita del service worker, è utile definire alcuni termini su come funziona il ciclo di vita.

Controllo e ambito

L'idea di controllo è fondamentale per comprendere come operano i Service worker. Una pagina descritta come controllata da un service worker è una pagina che consente a un service worker di intercettare le richieste di rete per suo conto. Il service worker è presente e in grado di lavorare per la pagina all'interno di un determinato ambito.

Ambito

L'ambito di un service worker è determinato dalla sua posizione su un server web. Se un service worker viene eseguito su una pagina situata in /subdir/index.html e si trova in /subdir/sw.js, l'ambito del service worker è /subdir/. Per vedere il concetto di ambito in azione, guarda questo esempio:

  1. Vai all'indirizzo https://service-worker-scope-viewer.glitch.me/subdir/index.html. Viene visualizzato un messaggio che indica che nessun service worker sta controllando la pagina. Tuttavia, nella pagina viene registrato un service worker di https://service-worker-scope-viewer.glitch.me/subdir/sw.js.
  2. Ricarica la pagina. Poiché il service worker è stato registrato ed è ora attivo, controlla la pagina. Sarà visibile un modulo contenente l'ambito, lo stato attuale e il relativo URL del service worker. Nota: dover ricaricare la pagina non ha nulla a che fare con l'ambito, ma con il ciclo di vita del service worker, che verrà spiegato più avanti.
  3. Ora vai all'indirizzo https://service-worker-scope-viewer.glitch.me/index.html. Anche se un service worker è stato registrato su questa origine, viene visualizzato ancora un messaggio che indica che non è attualmente presente alcun service worker. Il motivo è che questa pagina non rientra nell'ambito del service worker registrato.

L'ambito limita le pagine controllate dal service worker. In questo esempio, ciò significa che il service worker caricato da /subdir/sw.js può controllare solo le pagine che si trovano nella /subdir/ o nella relativa struttura ad albero secondaria.

Quanto sopra è descritto come funziona l'ambito per impostazione predefinita, ma l'ambito massimo consentito può essere sostituito impostando l'intestazione della risposta Service-Worker-Allowed e passando un'opzione scope al metodo register.

A meno che non ci sia un buon motivo per limitare l'ambito del service worker a un sottoinsieme di un'origine, carica un service worker dalla directory radice del server web in modo che l'ambito sia il più ampio possibile e non preoccuparti dell'intestazione Service-Worker-Allowed. Così è molto più semplice per tutti.

Client

Quando si dice che un service worker controlla una pagina, in realtà sta controllando un client. Un client è una qualsiasi pagina aperta il cui URL rientra nell'ambito di quel service worker. Nello specifico, si tratta di istanze di una WindowClient.

Il ciclo di vita di un nuovo service worker

Per far sì che un service worker possa controllare una pagina, devi prima crearla, per così dire. Iniziamo con cosa succede quando viene eseguito il deployment di un service worker completamente nuovo per un sito web senza un service worker attivo.

Iscrizione

La registrazione è il passaggio iniziale del ciclo di vita del service worker:

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

Questo codice viene eseguito nel thread principale e svolge le seguenti operazioni:

  1. Poiché la prima visita dell'utente a un sito web avviene senza un service worker registrato, attendi che la pagina venga caricata completamente prima di registrarne uno. Ciò consente di evitare conflitti della larghezza di banda se il service worker pre-memorizza nella cache dei dati.
  2. Sebbene il service worker sia ben supportato, un rapido controllo consente di evitare errori nei browser in cui non è supportato.
  3. Quando la pagina è completamente caricata e se il service worker è supportato, registra /sw.js.

Alcuni aspetti fondamentali da comprendere sono:

  • I Service worker sono disponibili solo tramite HTTPS o localhost.
  • Se i contenuti di un service worker contengono errori di sintassi, la registrazione non va a buon fine e il service worker viene eliminato.
  • Promemoria: i Service worker operano in un ambito. Qui, l'ambito è l'intera origine, poiché è stato caricato dalla directory root.
  • All'inizio della registrazione, lo stato del service worker viene impostato su 'installing'.

Al termine della registrazione, inizia l'installazione.

Installazione

Un service worker attiva il suo evento install dopo la registrazione. install viene chiamato una sola volta per ogni service worker e non si attiverà finché non viene aggiornato. Un callback per l'evento install può essere registrato nell'ambito del worker con addEventListener:

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

Viene creata una nuova istanza Cache e gli asset vengono pre-memorizzati nella cache. Ci saranno molte opportunità per parlare della pre-memorizzazione nella cache in un secondo momento, quindi concentriamoci sul ruolo di event.waitUntil. event.waitUntil accetta una promessa e attende che venga risolta. In questo esempio, la promessa svolge due operazioni asincrone:

  1. Crea una nuova istanza di Cache denominata 'MyFancyCache_v1'.
  2. Dopo la creazione della cache, un array di URL di asset viene pre-memorizzato nella cache utilizzando il suo metodo addAll asincrono.

L'installazione non va a buon fine se le promesse passate a event.waitUntil vengono rifiutate. In questo caso, il service worker viene eliminato.

Se la promessa viene resolve, l'installazione va a buon fine e lo stato del service worker cambierà in 'installed' e quindi verrà attivato.

Attivazione

Se la registrazione e l'installazione hanno esito positivo, il service worker si attiva e il suo stato diventa 'activating' Il lavoro può essere completato durante l'attivazione nell'evento activate del service worker. Un'attività tipica in questo evento è l'eliminazione delle vecchie cache, ma per un service worker completamente nuovo, questa funzionalità non è al momento pertinente e verrà ampliata quando parleremo degli aggiornamenti dei service worker.

Per i nuovi service worker, activate si attiva subito dopo l'esito positivo di install. Al termine dell'attivazione, lo stato del service worker diventa 'activated'. Tieni presente che, per impostazione predefinita, il nuovo service worker non inizierà a controllare la pagina fino al successivo aggiornamento della pagina o della navigazione.

Gestione degli aggiornamenti dei service worker

Dopo il deployment del primo service worker, dovrai probabilmente aggiornarlo in un secondo momento. Ad esempio, potrebbe essere necessario un aggiornamento se si verificano cambiamenti nella logica di gestione delle richieste o di pre-memorizzazione nella cache.

Quando vengono eseguiti aggiornamenti

I browser verificano la presenza di aggiornamenti per un service worker quando:

  • L'utente passa a una pagina all'interno dell'ambito del service worker.
  • navigator.serviceWorker.register() viene chiamato con un URL diverso dal service worker attualmente installato, ma non modificare l'URL di un service worker.
  • navigator.serviceWorker.register() viene chiamato con lo stesso URL del service worker installato, ma con un ambito diverso. Anche in questo caso, evita questo problema mantenendo l'ambito alla radice di un'origine, se possibile.
  • Quando sono stati attivati eventi come 'push' o 'sync' nelle ultime 24 ore, ma non devi ancora preoccuparti di questi eventi.

Come vengono eseguiti gli aggiornamenti

Sapere quando il browser aggiorna un service worker è importante, ma lo è anche il "come". Supponendo che l'URL o l'ambito di un service worker non siano modificati, un service worker attualmente installato esegue l'aggiornamento a una nuova versione solo se i suoi contenuti sono cambiati.

I browser rilevano i cambiamenti in due modi:

  • Eventuali modifiche byte per byte agli script richieste da importScripts, se applicabile.
  • Qualsiasi modifica al codice di primo livello del service worker, che influisce sull'impronta generata dal browser.

Il browser esegue molte operazioni. Per assicurarti che il browser abbia tutto ciò che serve per rilevare in modo affidabile le modifiche ai contenuti di un service worker, non comunicare alla cache HTTP di conservarlo e non modificare il nome del file. Il browser esegue automaticamente i controlli degli aggiornamenti in caso di navigazione verso una nuova pagina nell'ambito di un service worker.

Attivare manualmente i controlli degli aggiornamenti

Per quanto riguarda gli aggiornamenti, generalmente la logica di registrazione non dovrebbe cambiare. Tuttavia, un'eccezione potrebbe essere la presenza di sessioni di lunga durata su un sito web. Questo può accadere nelle applicazioni a pagina singola in cui le richieste di navigazione sono rare, dal momento che l'applicazione in genere riceve una sola richiesta di navigazione all'inizio del suo ciclo di vita. In queste situazioni, può essere attivato un aggiornamento manuale nel thread principale:

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

Per i siti web tradizionali o in tutti i casi in cui le sessioni utente non hanno una lunga durata, probabilmente l'attivazione di aggiornamenti manuali non è necessaria.

Installazione

Quando utilizzi un bundler per generare asset statici, questi asset contengono hash nel nome, ad esempio framework.3defa9d2.js. Supponiamo che alcune di queste risorse siano pre-memorizzate nella cache per l'accesso offline in un secondo momento. Ciò richiederebbe un aggiornamento del service worker per pre-memorizzare nella cache gli asset aggiornati:

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

Due cose differiscono dal primo esempio di evento install dal precedente:

  1. È stata creata una nuova istanza Cache con chiave 'MyFancyCacheName_v2'.
  2. I nomi delle risorse prememorizzate nella cache sono cambiati.

Una cosa da notare è che un service worker aggiornato viene installato insieme a quello precedente. Questo significa che il service worker precedente ha ancora il controllo di tutte le pagine aperte e, dopo l'installazione, il nuovo service worker rimane in attesa fino a quando non viene attivato.

Per impostazione predefinita, un nuovo service worker si attiva quando nessun client è controllato da quello precedente. Questo accade quando tutte le schede aperte del sito web pertinente sono chiuse.

Attivazione

Quando viene installato un service worker aggiornato e la fase di attesa termina, si attiva e il service worker precedente viene eliminato. Un'attività comune da eseguire nell'evento activate di un service worker aggiornato è l'eliminazione delle vecchie cache. Rimuovi le vecchie cache recuperando le chiavi per tutte le istanze Cache aperte con caches.keys ed eliminando le cache che non sono in una lista consentita definita con caches.delete:

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

Le vecchie cache non si riordinano. Dobbiamo farlo autonomamente se non rischi di superare le quote di archiviazione. Poiché l'elemento 'MyFancyCacheName_v1' del primo service worker non è aggiornato, la lista consentita della cache viene aggiornata specificando 'MyFancyCacheName_v2', il che elimina le cache con un nome diverso.

L'evento activate terminerà dopo la rimozione della vecchia cache. A questo punto, il nuovo service worker assumerà il controllo della pagina, sostituendo infine quello precedente.

Il ciclo di vita continua

Sia che Workbox venga utilizzato per gestire il deployment e gli aggiornamenti dei service worker o che venga utilizzata direttamente l'API Service Worker, è utile comprendere il ciclo di vita dei service worker. Con questa comprensione, i comportamenti dei Service worker dovrebbero sembrare più logici che misteriosi.

Se invece vuoi approfondire questo argomento, ti consigliamo di dare un'occhiata a questo articolo di Jake Archibald. L'intero ciclo di vita dei servizi è caratterizzato da un sacco di sfumature, ma è ben noto e le conoscenze necessarie saranno utili per l'utilizzo di Workbox.