Mentalità dei service worker

Come pensare ai worker di servizio.

I worker di servizio sono potenti e vale la pena impararli. Ti consentono di offrire ai tuoi utenti un livello di esperienza completamente nuovo. Il tuo sito può caricarsi istantaneamente. Può funzionare offline. Può essere installata come app specifica per piattaforma e offre la stessa esperienza, ma con la copertura e la libertà del web.

Tuttavia, i worker di servizio sono diversi da tutto ciò a cui la maggior parte degli sviluppatori web è abituata. Hanno una curva di apprendimento ripida e alcuni problemi da tenere d'occhio.

Di recente, Google Developers e io abbiamo collaborato a un progetto, Service Workies, un gioco senza costi per comprendere i worker di servizio. Durante la creazione e la gestione delle complesse ins e out dei worker di servizio, ho riscontrato alcuni problemi. Ciò che mi ha aiutato di più è stato trovare una manciata di metafore descrittive. In questo post esploreremo questi modelli mentali e cercheremo di comprendere le caratteristiche paradossali che rendono i worker sia complicati che fantastici.

Lo stesso, ma diverso

Durante la codifica del tuo worker di servizio, molte cose ti sembreranno familiari. Puoi utilizzare le nuove funzionalità del linguaggio JavaScript che preferisci. Ascolta gli eventi del ciclo di vita come per gli eventi dell'interfaccia utente. Gestisci il flusso di controllo con le promesse come sempre.

Ma altri comportamenti dei worker del servizio ti lasciano perplesso. Soprattutto quando aggiorni la pagina e non vedi le modifiche al codice applicate.

Un nuovo livello

Normalmente, quando crei un sito, devi considerare solo due livelli: il client e il server. Il service worker è un nuovo livello intermedio.

Un worker di servizio funge da livello intermedio tra il client e il server

Pensa al tuo service worker come a una sorta di estensione del browser, che il tuo sito può installare nel browser dell'utente. Una volta installato, il service worker espande il browser per il tuo sito con un potente livello intermedio. Questo livello di worker di servizio può intercettare e gestire tutte le richieste effettuate dal tuo sito.

Il livello del worker del servizio ha un proprio ciclo di vita indipendente dalla scheda del browser. Un semplice aggiornamento della pagina non è sufficiente per aggiornare un worker di servizio, proprio come non ti aspetteresti che un aggiornamento della pagina aggiorni il codice di cui è stato eseguito il deployment su un server. Ogni livello ha le proprie regole uniche per l'aggiornamento.

Nel gioco Service Workies vengono trattati i molti dettagli del ciclo di vita dei worker di servizio e ti offriamo un'ampia possibilità di esercitarti con questo tipo di worker.

Potente, ma con limitazioni

Avere un service worker sul tuo sito offre vantaggi incredibili. Il tuo sito può:

  • Funzionare perfettamente anche quando l'utente è offline
  • ottenere enormi miglioramenti delle prestazioni tramite la cache
  • utilizzare le notifiche push
  • essere installata come PWA

Nonostante le loro potenzialità, i worker di servizio sono limitati per progettazione. Non possono eseguire operazioni sincrone o nello stesso thread del tuo sito. Ciò significa che non potrai accedere a:

  • localStorage
  • il DOM
  • la finestra

La buona notizia è che esistono diversi modi in cui la tua pagina può comunicare con il proprio worker di servizio, tra cui postMessage diretti, canali di messaggistica uno a uno e canali di trasmissione uno a molti.

Lunga esecuzione, ma di breve durata

Un worker di servizio attivo continua a funzionare anche dopo che un utente esce dal tuo sito o chiude la scheda. Il browser mantiene questo worker di servizio in modo che sia pronto la volta successiva che l'utente torna sul tuo sito. Prima che venga effettuata la prima richiesta, il service worker ha la possibilità di intercettarla e prendere il controllo della pagina. Questo è ciò che consente a un sito di funzionare offline: il service worker può pubblicare una versione memorizzata nella cache della pagina stessa, anche se l'utente non ha una connessione a internet.

In Service Workies visualizziamo questo concetto con Kolohe (un simpatico worker) che intercetta e gestisce le richieste.

Arrestata

Sebbene i worker di servizio appaiano immortali, possono essere interrotti quasi in qualsiasi momento. Il browser non vuole sprecare risorse su un service worker che al momento non fa nulla. L'interruzione non è la stessa cosa dell'interruzione: il worker del servizio rimane installato e attivato. È solo in sospensione. La volta successiva che è necessario (ad esempio per gestire una richiesta), il browser lo riattiva.

waitUntil

A causa della costante possibilità di essere messo in sospensione, il tuo service worker ha bisogno di un modo per comunicare al browser quando sta facendo qualcosa di importante e non ha voglia di fare un pisolino. Ed è qui che entra in gioco event.waitUntil(). Questo metodo estende il ciclo di vita in cui viene utilizzato, impedendo che venga interrotto e che passi alla fase successiva del ciclo di vita finché non saremo pronti. Questo ci dà il tempo di configurare le cache, recuperare le risorse dalla rete e così via.

Questo esempio indica al browser che l'installazione del nostro worker dei servizi non è completata finché la cache assets non è stata creata e compilata con l'immagine di una spada:

self.addEventListener("install", event => {
  event.waitUntil(
    caches.open("assets").then(cache => {
      return cache.addAll(["/weapons/sword/blade.png"]);
    })
  );
});

Fai attenzione allo stato globale

Quando si verifica questo avvio/arresto, l'ambito globale del worker del servizio viene reimpostato. Fai quindi attenzione a non utilizzare alcun stato globale nel tuo worker di servizio, altrimenti rimarrai deluso la prossima volta che si riattiva e avrà uno stato diverso da quello previsto.

Considera questo esempio che utilizza uno stato globale:

const favoriteNumber = Math.random();
let hasHandledARequest = false;

self.addEventListener("fetch", event => {
  console.log(favoriteNumber);
  console.log(hasHandledARequest);
  hasHandledARequest = true;
});

Per ogni richiesta, questo worker di servizio registrerà un numero, diciamo 0.13981866382421893. Anche la variabile hasHandledARequest diventa true. Ora il service worker rimane inattivo per un po', quindi il browser lo arresta. La volta successiva che viene inviata una richiesta, il service worker è di nuovo necessario, quindi il browser lo riattiva. Lo script viene valutato di nuovo. Ora hasHandledARequest viene reimpostato su false e favoriteNumber è qualcosa di completamente diverso: 0.5907281835659033.

Non puoi fare affidamento sullo stato memorizzato in un worker di servizio. Inoltre, la creazione di istanze di elementi come i canali di messaggi può causare bug: ogni volta che il service worker si arresta/avvia, viene creata una nuova istanza.

Nel capitolo 3 di Service Workers, visualizziamo il nostro service worker fermo che perde tutto il colore mentre attende di essere riattivato.

Visualizzazione di un worker di servizio interrotto

Insieme, ma separati

La tua pagina può essere controllata da un solo service worker alla volta. Tuttavia, può avere due service worker installati contemporaneamente. Quando apporti una modifica al codice del tuo service worker e aggiorni la pagina, non stai effettivamente modificando il service worker. I worker di servizio sono immutati. ma ne stai creando una nuova. Questo nuovo worker di servizio (chiamiamolo SW2) verrà installato, ma non verrà ancora attivato. Deve wait che il service worker corrente (SW1) venga interrotto (quando l'utente esce dal sito).

Interferire con le cache di un altro service worker

Durante l'installazione, SW2 può configurare le impostazioni, in genere creando e compilando le cache. Tieni presente che questo nuovo worker di servizio ha accesso a tutto ciò a cui ha accesso l'attuale worker di servizio. Se non fai attenzione, il nuovo service worker in attesa può davvero rovinare tutto per il tuo attuale service worker. Ecco alcuni esempi che potrebbero causare problemi:

  • SW2 potrebbe eliminare una cache in uso da SW1.
  • SW2 potrebbe modificare i contenuti di una cache utilizzata da SW1, facendo sì che SW1 risponda con asset che la pagina non si aspetta.

Salta skipWaiting

Un service worker può anche utilizzare il metodo rischioso skipWaiting() per assumere il controllo della pagina non appena è terminata l'installazione. In genere, questa non è una buona idea, a meno che tu non stia cercando intenzionalmente di sostituire un worker di servizio con bug. Il nuovo worker di servizio potrebbe utilizzare risorse aggiornate che la pagina corrente non prevede, causando errori e bug.

Inizia con una base pulita

Per evitare che i tuoi worker dei servizi si sovrappongano, assicurati che utilizzino cache diverse. Il modo più semplice per farlo è creare una versione dei nomi delle cache utilizzate.

const version = 1;
const assetCacheName = `assets-${version}`;

self.addEventListener("install", event => {
  caches.open(assetCacheName).then(cache => {
    // confidently do stuff with your very own cache
  });
});

Quando esegui il deployment di un nuovo service worker, esegui l'upgrade di version in modo che esegua le operazioni necessarie con una cache completamente separata dal service worker precedente.

visualizzazione di una cache

Pulizia finale

Quando il tuo service worker raggiunge lo stato activated, sai che ha preso il controllo e che il service worker precedente è ridondante. A questo punto è importante eseguire la pulizia dopo il vecchio service worker. Non solo rispetta i limiti di spazio di archiviazione della cache degli utenti, ma può anche prevenire bug involontari.

Il metodo caches.match() è una scorciatoia spesso utilizzata per recuperare un elemento da qualsiasi cache in cui è presente una corrispondenza. ma esegue l'iterazione delle cache nell'ordine in cui sono state create. Supponiamo che tu abbia due versioni di un file di script app.js in due cache diverse: assets-1 e assets-2. La pagina si aspetta lo script più recente memorizzato in assets-2. Tuttavia, se non hai eliminato la vecchia cache, caches.match('app.js') restituirà quella vecchia di assets-1 e molto probabilmente il tuo sito non funzionerà.

Per eseguire la pulizia dopo i service worker precedenti, è sufficiente eliminare la cache non necessaria per il nuovo service worker:

const version = 2;
const assetCacheName = `assets-${version}`;

self.addEventListener("activate", event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== assetCacheName){
            return caches.delete(cacheName);
          }
        });
      );
    });
  );
});

Impedire ai tuoi service worker di sovrapporsi richiede un po' di lavoro e disciplina, ma vale la pena.

Mentalità dei service worker

Acquisire la mentalità giusta quando pensi ai worker ti aiuterà a crearne uno con sicurezza. Una volta acquisita la necessaria dimestichezza, potrai creare esperienze incredibili per i tuoi utenti.

Se vuoi capire tutto questo giocando, hai trovato quello che fa per te. Gioca a Service Workies, dove imparerai a utilizzare i worker di servizio per sconfiggere le bestie offline.