Ciclo di vita del service worker

Jake Archibald
Jake Archibald

Il ciclo di vita del service worker è la parte più complicata. Se non sai cosa sta cercando di fare e quali sono i vantaggi, può sembrare un po' contro di te. Se però sai come funziona, potrai fornire agli utenti aggiornamenti immediati e discreti, combinando il meglio dei pattern web e nativi.

Questo è un approfondimento, ma i punti elenco all'inizio di ogni sezione coprono gran parte di ciò che devi sapere.

L'intento

Lo scopo del ciclo di vita è:

  • Rendi possibile la modalità offline.
  • Consenti a un nuovo service worker di prepararsi senza interrompere quello attuale.
  • Assicurati che una pagina nell'ambito sia controllata dallo stesso service worker (o nessun service worker) durante l'intero ambito.
  • Assicurati che sia in esecuzione una sola versione del sito alla volta.

Quest'ultimo è piuttosto importante. Senza i service worker, gli utenti possono caricare una scheda sul sito e aprirne un'altra in un secondo momento. Di conseguenza, vengono eseguite contemporaneamente due versioni del sito. A volte va bene, ma se hai a che fare con lo spazio di archiviazione potresti facilmente ritrovarti con due schede che hanno opinioni molto diverse su come dovrebbe essere gestito lo spazio di archiviazione condiviso. Questo può causare errori o, peggio ancora, la perdita di dati.

Il primo service worker

In breve:

  • L'evento install è il primo evento ricevuto da un service worker e si verifica una sola volta.
  • Una promessa passata a installEvent.waitUntil() indica la durata e l'esito positivo o negativo dell'installazione.
  • Un service worker non riceverà eventi come fetch e push finché non completa l'installazione e diventa "attivo".
  • Per impostazione predefinita, i recuperi di una pagina vengono eseguiti tramite un service worker, a meno che la richiesta stessa non sia stata eseguita da un service worker. Dovrai quindi aggiornare la pagina per vedere gli effetti del service worker.
  • clients.claim() può eseguire l'override di questa impostazione predefinita e assumere il controllo delle pagine non controllate.

Prendi questo codice HTML:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Registra un service worker e aggiunge l'immagine di un cane dopo 3 secondi.

Ecco il suo service worker, sw.js:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

Memorizza nella cache l'immagine di un gatto e la pubblica ogni volta che c'è una richiesta per /dog.svg. Tuttavia, se esegui l'esempio riportato sopra, vedrai un cane la prima volta che carichi la pagina. Premi Aggiorna e vedrai il gatto.

Ambito e controllo

L'ambito predefinito della registrazione di un service worker è ./ in relazione all'URL dello script. Ciò significa che se registri un service worker //example.com/foo/bar.js, l'ambito predefinito sarà //example.com/foo/.

Noi chiamiamo pagine, lavoratori e i lavoratori condivisi clients. Il service worker può controllare solo i client nell'ambito. Una volta che un client è "controllato", i suoi recuperi passano attraverso il service worker nell'ambito. Puoi rilevare se un client è controllato tramite navigator.serviceWorker.controller, il che sarà null o un'istanza di service worker.

Scaricare, analizzare ed eseguire

Il tuo primo service worker viene scaricato quando chiami .register(). Se lo script non viene scaricato o analizzato oppure genera un errore durante l'esecuzione iniziale, la promessa del registro viene rifiutata e il service worker viene eliminato.

Gli strumenti DevTools di Chrome mostrano l'errore nella console e nella sezione Service worker della scheda Applicazione:

Errore visualizzato nella scheda DevTools dei Service worker

Installa

Il primo evento ricevuto da un service worker è install. Viene attivato non appena il worker viene eseguito e viene chiamato una sola volta per service worker. Se modifichi lo script del service worker, il browser lo considera come un service worker diverso e riceverà il proprio evento install. Parleremo degli aggiornamenti in dettaglio più avanti.

L'evento install è la tua possibilità di memorizzare nella cache tutto ciò di cui hai bisogno prima di poter controllare i client. La promessa che passi a event.waitUntil() consente al browser di sapere quando l'installazione è stata completata e se è riuscita.

Se la tua promessa viene rifiutata, l'installazione non è riuscita e il browser scarta il service worker. Non controllerà mai i clienti. Questo significa che non possiamo fare affidamento sulla presenza di cat.svg nella cache nei nostri eventi fetch. È una dipendenza.

Attivazione

Quando il service worker è pronto a controllare i client e a gestire eventi funzionali come push e sync, riceverai un evento activate. Ciò non significa che verrà controllata la pagina con il nome .register().

La prima volta che carichi la demo, anche se dog.svg viene richiesto molto tempo dopo l'attivazione del service worker, quest'ultimo non gestisce la richiesta e vedi ancora l'immagine del cane. L'impostazione predefinita è la coerenza: se la pagina viene caricata senza un service worker, nemmeno le relative sottorisorse. Se carichi la demo una seconda volta (ovvero aggiorni la pagina), il controllo viene controllato. Sia la pagina che l'immagine testeranno gli eventi fetch e vedrai un gatto.

clients.claim

Puoi assumere il controllo dei client non controllati chiamando clients.claim() all'interno del service worker dopo che è stato attivato.

Ecco una variante della demo sopra riportata che chiama clients.claim() nel suo evento activate. Dovresti vedere un gatto la prima volta. Dico "dovrebbe", perché questo fattore è sensibile alle tempistiche. Vedrai un gatto solo se il service worker si attiva e clients.claim() diventa effettivo prima del tentativo di caricamento dell'immagine.

Se utilizzi il service worker per caricare pagine in modo diverso rispetto a quelle che caricherebbero tramite la rete, clients.claim() può essere problematico, dato che il service worker finisce per controllare alcuni client che sono stati caricati senza il nodo stesso.

Aggiornamento del service worker

In breve:

  • Viene attivato un aggiornamento se si verifica una delle seguenti condizioni:
    • Una navigazione verso una pagina nell'ambito.
    • Un evento funzionale come push e sync, a meno che non sia stata eseguita una verifica degli aggiornamenti nelle 24 ore precedenti.
    • Chiamata a .register() solo se l'URL del service worker è cambiato. Tuttavia, dovresti evitare di modificare l'URL del worker.
  • La maggior parte dei browser, incluso Chrome 68 e versioni successive, ignora per impostazione predefinita le intestazioni della memorizzazione nella cache quando verificano la presenza di aggiornamenti dello script del service worker registrato. Rispettano comunque le intestazioni di memorizzazione nella cache durante il recupero delle risorse caricate all'interno di un service worker tramite importScripts(). Puoi ignorare questo comportamento predefinito impostando l'opzione updateViaCache durante la registrazione del service worker.
  • Il service worker viene considerato aggiornato se è diverso in base al byte rispetto a quello già presente nel browser. Stiamo estendendo questa funzionalità per includere anche gli script/moduli importati.
  • Il service worker aggiornato viene avviato insieme a quello esistente e riceve il proprio evento install.
  • Se il codice di stato del nuovo worker ha un codice di stato non corretto (ad esempio, 404), non riesce ad analizzare, genera un errore durante l'esecuzione o lo rifiuta durante l'installazione, il nuovo worker viene eliminato, ma quello attuale rimane attivo.
  • Una volta installato correttamente, il worker aggiornato wait fino a quando il worker esistente non controllerà zero client. Tieni presente che i client si sovrappongono durante un aggiornamento.
  • self.skipWaiting() impedisce l'attesa, il che significa che il service worker si attiva al termine dell'installazione.

Supponiamo di aver modificato lo script del nostro service worker in modo da rispondere con l'immagine di un cavallo anziché di un gatto:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

Guarda una demo di quanto riportato sopra. Dovresti comunque vedere l'immagine di un gatto. Ecco perché...

Installa

Tieni presente che ho cambiato il nome della cache da static-v1 a static-v2. Questo significa che posso configurare la nuova cache senza sovrascrivere quella attuale, che viene ancora utilizzata dal vecchio service worker.

Questo pattern crea cache specifiche per la versione, in modo simile agli asset che un'app nativa raggruppa con il relativo eseguibile. Potresti anche avere cache non specifiche per la versione, ad esempio avatars.

In attesa

Dopo essere stato installato correttamente, il service worker aggiornato ritarda l'attivazione fino a quando il service worker esistente non controlla più i client. Questo stato è chiamato "in attesa" ed è il modo in cui il browser assicura che sia in esecuzione una sola versione del service worker alla volta.

Se hai eseguito la demo aggiornata, dovresti comunque vedere l'immagine di un gatto, perché il worker V2 non è ancora stato attivato. Puoi vedere il nuovo service worker in attesa nella scheda "Applicazione" di DevTools:

DevTools che mostra un nuovo service worker in attesa

Anche se hai una sola scheda aperta per la demo, aggiornare la pagina non è sufficiente per lasciare che la nuova versione prenda il controllo. Ciò è dovuto al funzionamento delle navigazioni nel browser. Quando navighi, la pagina corrente non scompare finché non sono state ricevute le intestazioni della risposta e anche in questo caso la pagina corrente potrebbe rimanere se la risposta ha un'intestazione Content-Disposition. A causa di questa sovrapposizione, il service worker attuale controlla sempre un client durante un aggiornamento.

Per scaricare l'aggiornamento, chiudi o esci da tutte le schede utilizzando il service worker attuale. In seguito, quando torni di nuovo alla demo, dovresti vedere il cavallo.

Questo pattern è simile a come si aggiorna Chrome. Gli aggiornamenti di Chrome vengono scaricati in background, ma non vengono applicati finché Chrome non viene riavviato. Nel frattempo, puoi continuare a utilizzare la versione corrente senza interruzioni. Tuttavia, questo è un problema durante lo sviluppo, ma DevTools ha dei modi per semplificarlo, di cui parlerò più avanti in questo articolo.

Attivazione

Viene attivato quando il precedente service worker è andato a buon fine e il nuovo service worker è in grado di controllare i client. Questo è il momento ideale per svolgere operazioni che non potevi fare mentre il precedente worker era ancora in uso, ad esempio la migrazione dei database e lo svuotamento delle cache.

Nella demo in alto, tengo un elenco di cache che mi aspettavo di trovare lì e, nell'evento di activate, mi sbarazzo di tutte le altre, rimuovendo la vecchia cache di static-v1.

Se passi una promessa a event.waitUntil(), gli eventi funzionali (fetch, push, sync e così via) verranno bufferizzati finché la promessa non viene risolta. Di conseguenza, quando si attiva l'evento fetch, l'attivazione è completamente completata.

Saltare la fase di attesa

La fase di attesa significa che stai eseguendo una sola versione del sito alla volta, ma se non hai bisogno di questa funzionalità, puoi attivare prima il nuovo service worker chiamando il numero self.skipWaiting().

Questo fa sì che il service worker escluda il worker attivo corrente e si attivi non appena entra nella fase di attesa (o immediatamente se è già nella fase di attesa). Non fa sì che il lavoratore salti l'installazione, ma resti in attesa.

Non ha importanza quando chiami skipWaiting(), purché sia durante o prima dell'attesa. È piuttosto comune chiamarlo nell'evento install:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

Puoi però chiamarlo come risultato di postMessage() per il service worker. Ad esempio, vuoi skipWaiting() seguire un'interazione utente.

Ecco una demo che utilizza skipWaiting(). Dovresti vedere l'immagine di una mucca senza doverti allontanare. Come clients.claim() è una razza, quindi la tua mucca viene mostrata soltanto se il nuovo service worker recupera, installa e si attiva prima che la pagina provi a caricare l'immagine.

Aggiornamenti manuali

Come già detto, il browser verifica automaticamente la disponibilità di aggiornamenti dopo le navigazioni e gli eventi funzionali, ma puoi anche attivarli manualmente:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

Se prevedi che l'utente utilizzerà il tuo sito a lungo senza ricaricare il sito, ti consigliamo di chiamare update() a intervalli (ad esempio ogni ora).

Evita di modificare l'URL dello script del service worker

Se hai letto il mio post sulle best practice per la memorizzazione nella cache, ti consigliamo di assegnare a ogni versione del service worker un URL univoco. Non farlo. In genere si tratta di una prassi negativa per i service worker, è sufficiente aggiornare lo script nella posizione attuale.

Può generare un problema come il seguente:

  1. index.html registra sw-v1.js come service worker.
  2. sw-v1.js memorizza nella cache e gestisce index.html in modo da funzionare offline.
  3. Aggiorna index.html affinché registri la tua nuova sw-v2.js brillante.

Se esegui questa operazione, l'utente non riceverà mai sw-v2.js perché sw-v1.js pubblica la versione precedente di index.html dalla sua cache. È necessario aggiornare il service worker per aggiornarlo. Wow.

Tuttavia, per la demo riportata sopra, ho cambiato l'URL del service worker. In questo modo, ai fini della dimostrazione, è possibile passare da una versione all'altra. Non è una cosa che farei in produzione.

Sviluppo facile

Il ciclo di vita del service worker è pensato per l'utente, ma durante lo sviluppo è un po' complicato. Fortunatamente, hai a disposizione alcuni strumenti:

Aggiorna al ricaricamento

Questa è la mia preferita.

DevTools mostra &quot;aggiornamento al ricaricamento&quot;

Questo cambia il ciclo di vita in modo che sia a misura di sviluppatore. Ogni navigazione:

  1. Recupera il service worker.
  2. Installalo come nuova versione anche se è identico in base ai byte, il che significa che l'evento install viene eseguito e le cache vengono aggiornate.
  3. Salta la fase di attesa per attivare il nuovo service worker.
  4. Naviga nella pagina.

Ciò significa che riceverai gli aggiornamenti a ogni navigazione (incluso l'aggiornamento) senza dover ricaricare due volte o chiudere la scheda.

Non aspettare

DevTools mostra il messaggio &quot;Salta attesa&quot;

Se c'è un worker in attesa, puoi premere "Salta attesa" in DevTools per promuoverlo immediatamente su "attivo".

Maiusc-Ricarica

Se forzi la ricarica della pagina (shift-riload), il service worker viene ignorato completamente. Non sarà controllato. Questa funzionalità è inclusa nelle specifiche, perciò funziona in altri browser che supportano i service-worker.

Gestione degli aggiornamenti

Il service worker è stato progettato come parte del Web estendibile. L'idea è che noi, in qualità di sviluppatori di browser, riconosciamo di non essere migliori degli sviluppatori web nello sviluppo web. Pertanto, non dovremmo fornire API strette di alto livello che risolvano un determinato problema utilizzando pattern noi a noi piace, ma dovrebbero invece darti accesso alle funzioni vitali del browser e consentirti di farlo come preferisci, nel modo che funziona al meglio per i tuoi utenti.

Quindi, per abilitare il maggior numero possibile di pattern, l'intero ciclo di aggiornamento è osservabile:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

Il ciclo di vita continua

Come si può notare, conviene comprendere il ciclo di vita dei Service worker e, in questo modo, i comportamenti dei Service worker dovrebbero sembrare più logici e meno misteriosi. Queste informazioni ti daranno maggiore sicurezza man mano che esegui il deployment e l'aggiornamento dei service worker.