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 che ti stia ostacolando. Tuttavia, una volta che sai come funziona, puoi fornire agli utenti aggiornamenti fluidi e non invadenti, combinando il meglio dei pattern web e nativi.

Si tratta di un'analisi approfondita, ma i punti all'inizio di ogni sezione coprono la maggior parte di ciò che devi sapere.

L'intenzione

Lo scopo del ciclo di vita è:

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

L'ultimo è molto importante. Senza i worker di servizio, gli utenti possono caricare una scheda sul tuo sito e poi aprirne un'altra in un secondo momento. Ciò 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. Ciò può comportare errori o, peggio, la perdita di dati.

Il primo worker di servizio

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. Dovrai quindi aggiornare la pagina per vedere gli effetti del service worker.
  • clients.claim() può sostituire questo valore predefinito 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 un'immagine di un gatto e la pubblica ogni volta che viene richiesta /dog.svg. Tuttavia, se esegui l'esempio precedente, la prima volta che caricherai la pagina vedrai un cane. Premi Aggiorna e potrai vedere il gatto.

Ambito e controllo

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

Chiamiamo pagine, worker e worker condivisi clients. Il tuo service worker può controllare solo i client che rientrano nell'ambito. Una volta che un client è "controllato", i relativi recuperi passano attraverso il worker di servizio nell'ambito. Puoi rilevare se un client è controllato tramite navigator.serviceWorker.controller, che sarà nullo o un'istanza di service worker.

Scarica, analizza ed esegui

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 ignorato.

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

Errore visualizzato nella scheda DevTools del servizio worker

Installa

Il primo evento ricevuto da un service worker è install. Viene attivato non appena viene eseguito il worker e viene chiamato una sola volta per service worker. Se modifichi lo script del tuo service worker, il browser lo considera un altro service worker e riceverà il proprio evento install. Tratterò gli 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 è completata e se è andata a buon fine.

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

Attiva

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

La prima volta che carichi la demo, anche se dog.svg viene richiesto molto tempo dopo l'attivazione del service worker, la richiesta non viene gestita e continui a vedere l'immagine del cane. Il valore predefinito è coerenza. Se la pagina viene caricata senza un worker di servizio, non verranno caricate nemmeno le relative risorse secondarie. Se carichi la demo una seconda volta (in altre parole, aggiorni la pagina), verrà controllata. Sia la pagina che l'immagine passeranno per gli eventi fetch e vedrai un gatto.

clients.claim

Puoi assumere il controllo dei client non controllati chiamando clients.claim() all'interno del tuo service worker una volta attivato.

Ecco una variante della demo precedente che chiama clients.claim() nel suo evento activate. Dovresti vedere un gatto la prima volta. Dico "dovrebbe" perché i tempi sono importanti. Vedrai un gatto solo se il worker del servizio si attiva e clients.claim() viene applicato prima che l'immagine provi a caricarsi.

Se utilizzi il tuo worker di servizio per caricare le pagine in modo diverso da come verrebbero caricate tramite la rete, clients.claim() può essere problematico, in quanto il tuo worker di servizio finisce per controllare alcuni client che si sono caricati senza.

Aggiornamento del service worker

In breve:

  • Viene attivato un aggiornamento se si verifica una delle seguenti condizioni:
    • Una navigazione a una pagina in ambito.
    • Eventi funzionali come push e sync, a meno che non sia stato eseguito un controllo degli aggiornamenti nelle 24 ore precedenti.
    • Chiamata a .register() solo se l'URL del service worker è stato modificato. Tuttavia, devi evitare di modificare l'URL del worker.
  • La maggior parte dei browser, tra cui Chrome 68 e versioni successive, ignora per impostazione predefinita le intestazioni di memorizzazione nella cache durante la ricerca di aggiornamenti dello script del worker di servizio 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 quando registri il tuo 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à anche agli script/moduli importati).
  • Il worker di servizio 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 a eseguire l'analisi, genera un errore durante l'esecuzione o viene rifiutato durante l'installazione, il nuovo worker viene ignorato, ma quello corrente rimane attivo.
  • Una volta installato correttamente, il worker aggiornato wait finché il worker esistente non controlla nessun 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 non appena termina l'installazione.

Supponiamo di aver modificato lo script del nostro worker di servizio in modo che risponda con un'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 sopra. Dovresti comunque vedere l'immagine di un gatto. Ecco perché...

Installa

Tieni presente che ho modificato il nome della cache da static-v1 a static-v2. Ciò significa che posso configurare la nuova cache senza sovrascrivere elementi in quella corrente, che viene ancora utilizzata dal vecchio service worker.

Questi pattern creano cache specifiche per la versione, simili agli asset che un'app nativa includerebbe nel proprio file eseguibile. Potrebbero anche essere presenti cache che non sono specifiche della versione, ad esempio avatars.

In attesa

Una volta installato correttamente, 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 si assicura che sia in esecuzione una sola versione del tuo service worker alla volta.

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

DevTools mostra il nuovo worker del servizio in attesa

Anche se hai aperto una sola scheda per la demo, l'aggiornamento della pagina non è sufficiente per consentire il passaggio alla nuova versione. Questo è dovuto al funzionamento delle navigazioni del browser. Quando navighi, la pagina corrente non scompare finché non vengono ricevute le intestazioni di risposta e, anche in questo caso, la pagina corrente potrebbe rimanere se la risposta ha un'intestazione Content-Disposition. A causa di questa sovrapposizione, l'attuale service worker controlla sempre un client durante un aggiornamento.

Per scaricare l'aggiornamento, chiudi o esci da tutte le schede utilizzando il service worker attuale. Quando tornerai alla demo, dovresti vedere il cavallo.

Questo modello è simile a quello 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 corrente senza interruzioni. Tuttavia, questa operazione è complicata durante lo sviluppo, ma DevTools offre dei modi per semplificarla, che verranno descritti più avanti in questo articolo.

Attiva

Viene attivato quando il vecchio service worker non è più presente 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, ad esempio la migrazione dei database e lo svuotamento delle cache.

Nella demo sopra, gestisco un elenco di cache che mi aspetto siano presenti e nell'evento activate elimino le altre, rimuovendo la vecchia cache 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. Pertanto, quando viene attivato l'evento fetch, l'attivazione è completamente completata.

Saltare la fase di attesa

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

In questo modo, il tuo service worker espellerà l'attuale worker attivo e si attiverà non appena entrerà nella fase di attesa (o immediatamente se è già in questa fase). Questo non comporta l'esclusione dell'installazione da parte del lavoratore, ma solo l'attesa.

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

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

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

Tuttavia, ti consigliamo di chiamarlo come risultato di un postMessage() al worker di servizio. Ad esempio, vuoi skipWaiting() dopo 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 accennato in precedenza, il browser controlla automaticamente la presenza 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 utilizzi il tuo sito per molto tempo senza ricaricarlo, ti consigliamo di chiamare update() a un intervallo (ad esempio ogni ora).

Evita di modificare l'URL dello script del tuo worker di servizio

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.

Potresti riscontrare un problema come questo:

  1. index.html registra sw-v1.js come worker di servizio.
  2. sw-v1.js memorizza nella cache e pubblica index.html, quindi funziona in modalità offline.
  3. Aggiorni index.html in modo che registri il tuo nuovo e scintillante sw-v2.js.

Se esegui la procedura descritta sopra, l'utente non riceve mai sw-v2.js, perché sw-v1.js pubblica la vecchia versione di index.html dalla cache. Ti sei messo in una posizione in cui devi aggiornare il tuo service worker per aggiornarlo. Bleah.

Tuttavia, per la demo qui sopra, ho modificato l'URL del service worker. In questo modo, per la demo, puoi 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 può essere complicato. Fortunatamente, esistono alcuni strumenti utili:

Aggiornamento al ricaricamento

Questa è la mia preferita.

DevTools mostra &quot;Aggiorna quando ricarica&quot;

In questo modo, il ciclo di vita diventa più adatto agli sviluppatori. Ogni navigazione:

  1. Recupera di nuovo il servizio worker.
  2. Installalo come nuova versione anche se è identico a livello di 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.

Saltare l'attesa

DevTools mostra &quot;Salta attesa&quot;

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

Maiusc-Ricarica

Se ricarichi forzatamente la pagina (ricarica con Maiusc), il service worker viene completamente ignorato. Non sarà controllato. Questa funzionalità è presente nella specifica, quindi funziona in altri browser che supportano i worker di servizio.

Gestione degli aggiornamenti

Il worker di servizio è stato progettato nell'ambito del web espandibile. L'idea è che noi, in qualità di sviluppatori di browser, riconosciamo di non essere più bravi nello sviluppo web rispetto agli sviluppatori web. Di conseguenza, non dovremmo fornire API di alto livello ristrette che risolvono un problema specifico utilizzando pattern che a noi piacciono, ma dovremmo darti accesso alle parti più interne del browser e consentirti di fare ciò che vuoi, nel modo più adatto ai tuoi utenti.

Pertanto, per attivare 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 conoscenze ti daranno maggiore sicurezza durante il deployment e l'aggiornamento dei worker di servizio.