Strategie per la memorizzazione nella cache dei service worker

Finora l'interfaccia di Cache contiene solo menzioni e piccoli snippet di codice. Per utilizzare i service worker in modo efficace, è necessario adottare una o più strategie di memorizzazione nella cache, che richiedono una certa familiarità con l'interfaccia Cache.

Una strategia di memorizzazione nella cache è un'interazione tra l'evento fetch di un service worker e l'interfaccia Cache. Il modo in cui viene scritta una strategia di memorizzazione nella cache dipende; ad esempio, potrebbe essere preferibile gestire le richieste di asset statici in modo diverso rispetto ai documenti e questo influisce sul modo in cui viene composta una strategia di memorizzazione nella cache.

Prima di approfondire le strategie stesse, dedichiamo un attimo a parlare di cosa non è l'interfaccia Cache, di cosa è e di un breve resoconto di alcuni dei metodi che offre per gestire le cache dei service worker.

Confronto tra l'interfaccia Cache e la cache HTTP

Se non hai mai lavorato con l'interfaccia Cache, potresti essere tentato di considerare questa interfaccia come la cache HTTP o almeno è correlata. Non è così.

  • L'interfaccia Cache è un meccanismo di memorizzazione nella cache completamente separato dalla cache HTTP.
  • Qualunque sia la configurazione di Cache-Control che utilizzi per influenzare la cache HTTP, non influisce su quali asset vengono memorizzati nell'interfaccia di Cache.

È utile pensare alle cache del browser come a più livelli. Si tratta di una cache di basso livello basata su coppie chiave-valore con istruzioni espresse nelle intestazioni HTTP.

L'interfaccia Cache, invece, è una cache di alto livello gestita da un'API JavaScript. Questo offre una maggiore flessibilità rispetto all'utilizzo di coppie chiave-valore HTTP relativamente semplici ed è la metà di ciò che rende possibili strategie di memorizzazione nella cache. Alcuni importanti metodi API per le cache dei service worker sono:

  • CacheStorage.open per creare una nuova istanza Cache.
  • Cache.add e Cache.put per archiviare le risposte di rete nella cache di un service worker.
  • Cache.match per individuare una risposta memorizzata nella cache in un'istanza Cache.
  • Cache.delete per rimuovere una risposta memorizzata nella cache da un'istanza Cache.

solo per citarne alcuni. Ci sono altri metodi utili, ma questi sono quelli di base che utilizzerai più avanti in questa guida.

L'umile evento fetch

L'altra metà di una strategia di memorizzazione nella cache è l'evento fetch del service worker. Finora in questa documentazione hai sentito parlare di "intercettazione delle richieste di rete" e l'evento fetch all'interno di un service worker è il luogo in cui si verifica:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', async (event) => {
  // Is this a request for an image?
  if (event.request.destination === 'image') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Respond with the image from the cache or from the network
      return cache.match(event.request).then((cachedResponse) => {
        return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
          // Add the network response to the cache for future visits.
          // Note: we need to make a copy of the response to save it in
          // the cache and use the original as the request response.
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

Questo è un esempio di giocattolo, che puoi vedere in azione, ma che offre un'idea di ciò che possono fare i Service worker. Il codice riportato sopra fa quanto segue:

  1. Controlla la proprietà destination della richiesta per verificare se si tratta di una richiesta di immagine.
  2. Se l'immagine si trova nella cache del service worker, pubblicala da lì. In caso contrario, recupera l'immagine dalla rete, memorizza la risposta nella cache e restituisci la risposta di rete.
  3. Tutte le altre richieste vengono trasmesse attraverso il service worker senza alcuna interazione con la cache.

L'oggetto event di un recupero contiene una proprietà request che contiene alcune informazioni utili per identificare il tipo di ogni richiesta:

  • url, che è l'URL della richiesta di rete attualmente gestita dall'evento fetch.
  • method, che è il metodo di richiesta (ad es. GET o POST).
  • mode, che descrive la modalità della richiesta. Il valore 'navigate' viene spesso utilizzato per distinguere le richieste di documenti HTML da altre richieste.
  • destination, che descrive il tipo di contenuti richiesti in modo da evitare di utilizzare l'estensione del file della risorsa richiesta.

Ancora una volta, l'asincronia è il nome del gioco. Ricorderai che l'evento install offre un metodo event.waitUntil che accetta una promessa e attende la sua risoluzione prima di continuare con l'attivazione. L'evento fetch offre un metodo event.respondWith simile, che puoi utilizzare per restituire il risultato di una richiesta fetch asincrona o una risposta restituita dal metodo match dell'interfaccia Cache.

Strategie di memorizzazione nella cache

Ora che hai acquisito un po' di familiarità con le istanze Cache e il gestore di eventi fetch, puoi approfondire alcune strategie di memorizzazione nella cache dei service worker. Anche se le possibilità sono praticamente infinite, questa guida si atterrà alle strategie fornite con Workbox, in modo da poter avere un'idea di ciò che accade all'interno di Workbox.

Solo cache

Mostra il flusso dalla pagina, al service worker, alla cache.

Iniziamo con una semplice strategia di memorizzazione nella cache che chiameremo "Solo cache". Ma è solo questo: quando il service worker ha il controllo della pagina, le richieste corrispondenti finiscono solo nella cache. Ciò significa che tutti gli asset memorizzati nella cache dovranno essere pre-memorizzati nella cache per essere disponibili affinché il pattern funzioni e che questi asset non verranno mai aggiornati nella cache finché non verrà aggiornato il service worker.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

// Assets to precache
const precachedAssets = [
  '/possum1.jpg',
  '/possum2.jpg',
  '/possum3.jpg',
  '/possum4.jpg'
];

self.addEventListener('install', (event) => {
  // Precache assets on install
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(precachedAssets);
  }));
});

self.addEventListener('fetch', (event) => {
  // Is this one of our precached assets?
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);

  if (isPrecachedRequest) {
    // Grab the precached asset from the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // Go to the network
    return;
  }
});

Sopra, un array di asset viene pre-memorizzato nella cache al momento dell'installazione. Quando il service worker gestisce i recuperi, controlliamo se l'URL della richiesta gestito dall'evento fetch rientra nell'array di asset pre-memorizzati nella cache. In questo caso, recuperiamo la risorsa dalla cache e ignoriamo la rete. Altre richieste passano alla rete, ma solo alla rete. Per vedere questa strategia in azione, guarda questa demo con la console aperta.

Solo rete

Mostra il flusso dalla pagina, al service worker, alla rete.

L'opposto di "Solo cache" è "Solo rete", in cui una richiesta viene trasmessa attraverso un service worker alla rete senza alcuna interazione con la cache del service worker. Questa è una buona strategia per garantire l'aggiornamento dei contenuti (pensa al markup), ma il compromesso è che non funzionerà mai quando l'utente è offline.

Assicurarti che una richiesta venga inviata alla rete significa semplicemente non chiamare event.respondWith per una richiesta corrispondente. Se vuoi essere esplicito, puoi aggiungere un return; vuoto nel callback dell'evento fetch per le richieste che vuoi trasmettere alla rete. Questo è ciò che accade nella demo della strategia "Solo cache" per le richieste che non sono pre-memorizzate nella cache.

Prima la cache, poi la rete

Mostra il flusso dalla pagina, al service worker, alla cache, quindi alla rete se non si trova nella cache.

È con questa strategia che le cose vengono un po' più coinvolte. Per le richieste con corrispondenza, la procedura è la seguente:

  1. La richiesta va a buon fine nella cache. Se l'asset si trova nella cache, pubblicalo da lì.
  2. Se la richiesta non è nella cache, vai alla rete.
  3. Al termine della richiesta di rete, aggiungila alla cache, quindi restituisci la risposta dalla rete.

Ecco un esempio di questa strategia, che puoi testare in una demo dal vivo:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a request for an image
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the cache first
      return cache.match(event.request.url).then((cachedResponse) => {
        // Return a cached response if we have one
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise, hit the network
        return fetch(event.request).then((fetchedResponse) => {
          // Add the network response to the cache for later visits
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

Anche se questo esempio riguarda solo le immagini, questa è un'ottima strategia da applicare a tutti gli asset statici (come CSS, JavaScript, immagini e caratteri), in particolare quelli con versione hash. Offre un aumento di velocità per gli asset immutabili, aggirando eventuali controlli di aggiornamento dei contenuti con il server e la memorizzazione nella cache HTTP. Soprattutto, tutti gli asset memorizzati nella cache saranno disponibili offline.

Prima la rete, poi passa alla cache

Mostra il flusso dalla pagina, al service worker, alla rete, quindi alla cache se la rete non è disponibile.

Se dovessi capovolgere "Prima la cache, poi la rete" hai come risultato la strategia "Prima rete, poi cache seconda", che è l'esempio seguente:

  1. Per prima cosa, vai alla rete per una richiesta, poi inserisci la risposta nella cache.
  2. Se sei offline in un secondo momento, eseguirai l'ultima versione della risposta nella cache.

Questa strategia è ottima per le richieste HTML o API quando, mentre sei online, vuoi la versione più recente di una risorsa, ma vuoi concedere l'accesso offline alla versione più recente disponibile. Ecco come potrebbe presentarsi una volta applicata alle richieste di HTML:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a navigation request
  if (event.request.mode === 'navigate') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the network first
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone());

        return fetchedResponse;
      }).catch(() => {
        // If the network is unavailable, get
        return cache.match(event.request.url);
      });
    }));
  } else {
    return;
  }
});

Puoi provarlo in una demo. Innanzitutto, vai alla pagina. Potrebbe essere necessario ricaricare la risposta HTML prima che venga inserita nella cache. Quindi, negli strumenti per sviluppatori, simula una connessione offline e ricarica di nuovo. L'ultima versione disponibile verrà pubblicata immediatamente dalla cache.

Nelle situazioni in cui la funzionalità offline è importante, ma è necessario bilanciarla con l'accesso alla versione più recente di un bit di markup o di dati dell'API, il metodo "Prima rete, poi cache" è una solida strategia per raggiungere questo obiettivo.

Riconvalida-mentre-inattivo

Mostra il flusso dalla pagina al service worker, alla cache, quindi dalla rete alla cache.

La più complessa è la strategia "inattiva durante la riconvalida", In qualche modo è simile alle ultime due strategie, ma la procedura dà la priorità alla velocità di accesso a una risorsa, pur mantenendola aggiornata in background. La strategia è simile al seguente:

  1. Alla prima richiesta di un asset, recuperalo dalla rete, posizionalo nella cache e restituisci la risposta di rete.
  2. Nelle richieste successive, pubblica prima l'asset dalla cache, quindi "in background", richiedilo di nuovo alla rete e aggiorna la voce della cache dell'asset.
  3. Per le richieste successive, riceverai l'ultima versione recuperata dalla rete che è stata inserita nella cache nel passaggio precedente.

Questa è un'ottima strategia per elementi che sono un po' importanti da tenere aggiornati, ma che non sono cruciali. Pensa a elementi come gli avatar per un sito di social media. Vengono aggiornati quando gli utenti completano questo processo, ma la versione più recente non è strettamente necessaria per ogni richiesta.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        });

        return cachedResponse || fetchedResponse;
      });
    }));
  } else {
    return;
  }
});

Puoi vederlo in azione in un'altra demo dal vivo, in particolare se presti attenzione alla scheda Network negli strumenti per sviluppatori del tuo browser e al suo visualizzatore CacheStorage (se negli strumenti per sviluppatori del tuo browser è presente uno strumento di questo tipo).

Passiamo a Workbox!

Questo documento riassume la nostra recensione dell'API del service worker e delle API correlate. Ciò significa che hai imparato abbastanza su come utilizzare direttamente i service worker per iniziare ad apportare modifiche con Workbox.