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) per tutto il tempo.
  • 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 facilmente ritrovarti 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 worker di servizio e si verifica una sola volta.
  • Una promessa passata a installEvent.waitUntil() indica la durata e il successo o l'errore dell'installazione.
  • Un worker del servizio non riceverà eventi come fetch e push finché non avrà completato l'installazione e non sarà "attivo".
  • Per impostazione predefinita, i recuperi di una pagina non passano attraverso un service worker, a meno che la richiesta della pagina stessa 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 prendere 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 relativo 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 riportato sopra, vedrai un cane la prima volta che carichi la pagina. Fai clic su Aggiorna e vedrai 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 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 worker di servizio è install. Viene attivato non appena viene eseguito il worker e viene chiamato una sola volta per service worker. Se modifichi lo script del tuo worker di servizio, il browser lo considera un worker di servizio diverso e riceverà il proprio evento install. Tratterò gli aggiornamenti in dettaglio più avanti.

L'evento install è la tua occasione per memorizzare nella cache tutto ciò che ti serve 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. Ciò significa che possiamo fare affidamento sulla presenza di cat.svg nella cache dei 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 sopra 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.
    • Chiama .register() solo se l'URL del service worker è cambiato. 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 worker di servizio è 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 worker del servizio si attiva non appena è terminata 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 ancora 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. Potresti anche avere cache non specifiche per la versione, ad esempio avatars.

In attesa

Una volta installato correttamente, il worker di servizio aggiornato ritarda l'attivazione finché il worker di servizio 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 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, il service worker corrente controlla sempre un client durante un aggiornamento.

Per ricevere l'aggiornamento, chiudi o esci da tutte le schede che utilizzano il servizio worker corrente. 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 finché Chrome non viene riavviato. 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(), gli eventi funzionali (fetch, push, sync e così via) verranno messi in coda 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). Non fa saltare l'installazione del tuo worker, ma attende.

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 dalla pagina. Come clients.claim(), è una gara, quindi vedrai la mucca solo se il nuovo service worker viene recuperato, installato e attivato 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, ti consigliamo di assegnare a ogni versione del tuo service worker un URL univoco. Non farlo. In genere, questa non è una buona pratica per i worker di servizio. Aggiorna semplicemente lo script nella posizione corrente.

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 trovi nella situazione in cui devi aggiornare il tuo service worker per aggiornare il tuo service worker. 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 è qualcosa che farei in produzione.

Semplificare lo sviluppo

Il ciclo di vita del service worker è progettato pensando all'utente, ma durante lo sviluppo è un po' 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 in modo che il nuovo worker di servizio venga attivato.
  4. Esplora la 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 hai un worker in attesa, puoi fare clic su "Salta attesa" in DevTools per promuoverlo immediatamente a "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 principali 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 continua

Come puoi vedere, vale la pena comprendere il ciclo di vita dei worker di servizio. Una volta compreso questo, i comportamenti dei worker di servizio dovrebbero sembrare più logici e meno misteriosi. Queste conoscenze ti daranno maggiore sicurezza durante il deployment e l'aggiornamento dei worker di servizio.