Ciclo di vita del service worker

Jake Archibald
Jake Archibald

Il ciclo di vita del service worker è la sua parte più complicata. Se non sai cosa sta cercando di fare e quali sono i vantaggi, puoi avere l'impressione che ti stia combattendo. Una volta appreso come funziona, puoi fornire agli utenti aggiornamenti immediati e discreti, unendo il meglio del web e dei pattern nativi.

Questo è un approfondimento, ma l'elenco puntato all'inizio di ogni sezione copre 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 che rientra nell'ambito sia controllata dallo stesso service worker (o da nessun service worker).
  • Assicurati che sia in esecuzione una sola versione del sito alla volta.

Quest'ultimo aspetto è molto importante. Senza i service worker, gli utenti possono caricare una scheda del sito e aprirne un'altra in un secondo momento. Questo può comportare l'esecuzione di due versioni del sito contemporaneamente. A volte va bene, ma se hai a che fare con lo spazio di archiviazione, potresti ritrovarti facilmente con due schede che hanno opinioni molto diverse su come gestire lo spazio di archiviazione condiviso. Questo può causare errori o, peggio, 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 il successo o l'errore dell'installazione.
  • Un service worker non riceverà eventi come fetch e push finché non termina l'installazione e diventa "attivo".
  • Per impostazione predefinita, i recuperi di una pagina non vengono eseguiti da un service worker, a meno che la richiesta della pagina non sia passata attraverso un service worker. Per vedere gli effetti del service worker, dovrai quindi aggiornare la pagina.
  • clients.claim() può sostituire questa impostazione predefinita e assumere il controllo delle pagine non controllate.

Usa 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 precedente, la prima volta che carichi la pagina vedrai un cane. Premi Aggiorna e potrai vedere il gatto.

Ambito e controllo

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

Noi definiamo pagine, worker e worker condivisi clients. Il tuo service worker può controllare solo i client che rientrano nell'ambito. Quando un client è "controllato", i suoi recuperi passano attraverso il service worker all'interno dell'ambito. Puoi rilevare se un client è controllato tramite navigator.serviceWorker.controller, che sarà nullo o un'istanza di service worker.

Download, analisi ed esecuzione

Il tuo primo service worker viene scaricato quando chiami .register(). Se lo script non riesce a scaricare, analizzare o genera un errore durante l'esecuzione iniziale, la promessa di registrazione viene rifiutata e il service worker viene eliminato.

DevTools di Chrome mostra l'errore nella console e nella sezione dei service worker della scheda dell'applicazione:

Errore visualizzato nella scheda DevTools dei service worker

Installa

Il primo evento ricevuto da un service worker è install. Viene attivato non appena viene eseguito il worker e viene chiamato solo una volta per ogni service worker. Se modifichi lo script del service worker, il browser lo considera 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 trasmetti a event.waitUntil() consente al browser di sapere quando l'installazione è stata completata e se è andata a buon fine.

Se la promessa viene rifiutata, significa che l'installazione non è riuscita e il browser scarta il service worker. Non controllerà mai i client. Questo significa che possiamo fare in modo che cat.svg sia presente nella cache nei nostri eventi fetch. È una dipendenza.

Attivazione

Quando il Service worker sarà pronto a controllare i client e a gestire eventi funzionali come push e sync, riceverai un evento activate. Tuttavia, questo non significa che la pagina chiamata .register() sarà controllata.

La prima volta che carichi la demo, anche se dog.svg viene richiesto molto tempo dopo l'attivazione del service worker, non gestisce la richiesta e continui a vedere l'immagine del cane. L'impostazione predefinita è la coerenza. Se la pagina viene caricata senza un service worker, neanche le relative risorse secondarie. Se carichi la demo una seconda volta (in altre parole, aggiorni la pagina), la demo viene controllata. Sia la pagina che l'immagine analizzano gli eventi fetch e vedrai invece un gatto.

clients.claim

Puoi assumere il controllo di client incontrollati chiamando clients.claim() all'interno del tuo service worker dopo l'attivazione.

Ecco una variante della demo precedente che chiama clients.claim() nel suo evento activate. La prima volta dovresti vedere un gatto. Dico "dovrebbe" perché è una questione di tempo. Vedrai un gatto solo se il service worker si attiva e clients.claim() diventa effettivo prima che l'immagine provi a caricarsi.

Se utilizzi il Service worker per caricare le pagine in modo diverso rispetto a quelle che verrebbero caricate tramite la rete, clients.claim() può rappresentare un problema, in quanto il tuo service worker finirà per controllare alcuni client che sono stati caricati senza di esso.

Aggiornamento del service worker

In breve:

  • Viene attivato un aggiornamento se si verifica una delle seguenti condizioni:
    • Una navigazione verso una pagina relativa all'ambito.
    • Un evento funzionante come push e sync, a meno che non sia stato effettuato un controllo degli aggiornamenti nelle precedenti 24 ore.
    • Chiamata a .register() solo se l'URL del service worker è stato modificato. Tuttavia, dovresti evitare di modificare l'URL del worker.
  • La maggior parte dei browser, inclusi Chrome 68 e versioni successive, ignora per impostazione predefinita le intestazioni di memorizzazione nella cache durante il controllo degli 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 tuo service worker viene considerato aggiornato se è diverso in termini di byte da 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 nuovo worker ha un codice di stato non OK (ad esempio, 404), non riesce ad analizzarlo, genera un errore durante l'esecuzione o rifiuta durante l'installazione, il nuovo worker viene scartato, ma quello attuale rimane attivo.
  • Una volta installato correttamente, il worker aggiornato wait finché il worker esistente non controlla zero client. Tieni presente che i clienti si sovrappongono durante un aggiornamento.
  • self.skipWaiting() impedisce l'attesa, il che significa che il service worker si attiva non appena termina l'installazione.

Supponiamo di aver modificato lo script del service worker in modo che risponda 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'));
  }
});

Dai un'occhiata a una demo di quanto illustrato 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. Ciò significa che posso configurare la nuova cache senza sovrascrivere quella attuale, che sta ancora utilizzando il vecchio service worker.

Questo pattern crea cache specifiche per la versione, un po' come gli asset che un'app nativa potrebbe raggruppare con il suo eseguibile. Potrebbero anche essere presenti cache che non sono specifiche della versione, ad esempio avatars.

In attesa

Una volta completata l'installazione, il service worker aggiornato ritarda l'attivazione finché il service worker esistente non controlla più i client. Questo stato è chiamato "in attesa" ed è il modo in cui il browser garantisce 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 si è ancora 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 basta per consentire alla nuova versione di assumere il controllo. Ciò è dovuto al funzionamento della navigazione nel browser. Quando navighi, la pagina corrente non scompare finché non vengono 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 corrente controlla sempre un client durante un aggiornamento.

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

Questo schema è simile all'aggiornamento di Chrome. Gli aggiornamenti di Chrome vengono scaricati in background, ma non vengono applicati fino al riavvio di Chrome. Nel frattempo, puoi continuare a utilizzare la versione attuale senza interruzioni. Questo, però, è un problema durante lo sviluppo, ma DevTools offre diversi modi per semplificarlo, cosa che vedremo più avanti in questo articolo.

Attivazione

Questo viene attivato una volta che il Service worker precedente è stato eliminato e il nuovo Service worker è in grado di controllare i client. Questo è il momento ideale per fare cose che non potevi fare mentre il vecchio worker era ancora in uso, come la migrazione dei database e svuotare le cache.

Nella demo riportata sopra, gestisco un elenco di cache che prevedo di trovare lì e nell'evento activate elimino tutte le altre, con la conseguente rimozione della vecchia cache di static-v1.

Se passi una promessa a event.waitUntil(), il buffering degli eventi funzionali (fetch, push, sync e così via) verrà eseguito fino alla risoluzione della promessa. Di conseguenza, quando si attiva l'evento fetch, l'attivazione è completamente completata.

Saltare la fase di attesa

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

Questo fa sì che il service worker espelle il worker attivo corrente e si attiva non appena entra nella fase di attesa (o immediatamente se è già in fase di attesa). Questo non comporta l'esclusione dell'installazione da parte del lavoratore, ma solo l'attesa.

Non ha importanza quando chiami skipWaiting(), purché lo sia durante l'attesa o prima di farlo. È piuttosto comune nominarlo nell'evento install:

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

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

ma potresti volerlo chiamare come risultato di postMessage() per il service worker. Ad esempio, vuoi skipWaiting() a seguito di un'interazione dell'utente.

Ecco una demo che utilizza skipWaiting(). Dovresti vedere l'immagine di una mucca senza dover uscire. Come clients.claim(), è una gara, quindi vedrai la mucca solo se il nuovo service worker recupera, installa e attiva prima che la pagina provi a caricare l'immagine.

Aggiornamenti manuali

Come già detto, il browser verifica automaticamente la presenza di aggiornamenti dopo navigazioni ed 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 per un lungo periodo di tempo senza ricaricare, puoi 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, puoi valutare di assegnare a ogni versione del tuo service worker un URL univoco. Non fare questo! Di solito si tratta di una prassi negativa per i service worker: è sufficiente aggiornare lo script nella sua posizione attuale.

Questo potrebbe portarti a un problema come questo:

  1. index.html registra sw-v1.js come service worker.
  2. sw-v1.js memorizza nella cache e gestisce index.html, quindi funziona prima offline.
  3. Aggiorna index.html in modo che registri il tuo sw-v2.js nuovo e brillante.

Se segui queste istruzioni, l'utente non riceverà mai sw-v2.js perché sw-v1.js sta gestendo la versione precedente di index.html dalla sua cache. Ti sei messo in una posizione in cui devi aggiornare il tuo service worker per aggiornarlo. Oh.

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

Semplificare lo sviluppo

Il ciclo di vita del service worker è pensato per l'utente, ma durante lo sviluppo è un po' complicato. Per fortuna esistono alcuni strumenti utili:

Aggiorna al ricaricamento

Questo è il mio preferito.

DevTools mostra &quot;aggiorna al ricaricamento&quot;

Questo cambia il ciclo di vita per facilitare gli sviluppatori. Ogni navigazione:

  1. Recupera il service worker.
  2. Installala come nuova versione anche se è identica 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 (compreso l'aggiornamento) senza dover ricaricare due volte o chiudere la scheda.

Niente più attese

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

Se c'è un worker in attesa, puoi fare clic su "Salta attesa" in DevTools per renderlo immediatamente "attivo".

Maiusc-ricarica

Se forzi il ricaricamento della pagina (shift-reload), il service worker viene ignorato. e non avrà alcun controllo. Questa funzionalità è presente nella specifica, quindi funziona in altri browser che supportano il worker-servizio.

Gestione degli aggiornamenti

Il service worker è stato progettato come parte del Web Extensible. L'idea è che noi sviluppatori di browser riconosciamo di non essere migliori nello sviluppo web degli sviluppatori web. Pertanto, non dovremmo fornire API di alto livello ristrette in grado di risolvere un particolare problema utilizzando pattern che ci piacciono, ma dare accesso all'istinto del browser e consentirti di farlo come vuoi, nel modo migliore 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 non finisce mai

Come puoi vedere, conviene comprendere il ciclo di vita dei service worker e, con questa comprensione, i comportamenti dei service worker dovrebbero sembrare più logici e meno misteriosi. Queste informazioni ti daranno maggiore sicurezza durante il deployment e l'aggiornamento dei Service worker.