Guida alla memorizzazione nella cache imperativa

Andrew Guan
Andrew Guan
Demián Renzulli
Demián Renzulli

Alcuni siti web potrebbero dover comunicare con il worker del servizio senza dover essere informati sul risultato. Ecco alcuni esempi:

  • Una pagina invia al service worker un elenco di URL da precaricare, in modo che, quando l'utente fa clic su un link, le risorse secondarie del documento o della pagina siano già disponibili nella cache, rendendo molto più rapida la navigazione successiva.
  • La pagina chiede al service worker di recuperare e memorizzare nella cache un insieme di articoli principali, in modo da metterli a disposizione per l'utilizzo offline.

La delega di questi tipi di attività non critiche al service worker ha il vantaggio di liberare il thread principale per gestire meglio le attività più urgenti, come la risposta alle interazioni utente.

Diagramma di una pagina che richiede risorse da memorizzare nella cache a un service worker.

In questa guida esploreremo come implementare una tecnica di comunicazione unidirezionale dalla pagina al servizio worker utilizzando le API del browser standard e la libreria Workbox. Chiameremo questi tipi di casi d'uso cache imperative.

Richiesta di produzione

1-800-Flowers.com ha implementato la cache imperativa (precaricamento) con i service worker tramite postMessage() per eseguire il pre-caricamento degli elementi principali nelle pagine delle categorie al fine di velocizzare la navigazione successiva alle pagine dei dettagli dei prodotti.

Logo di 1-800 Flowers.

Utilizzano un approccio misto per decidere quali elementi precaricare:

  • Al momento del caricamento della pagina, chiede al worker del fornitore di servizi di recuperare i dati JSON per i primi 9 elementi e di aggiungere gli oggetti di risposta risultanti alla cache.
  • Per gli elementi rimanenti, ascoltano l'evento mouseover , in modo che, quando un utente sposta il cursore sopra un elemento, possa attivare un recupero della risorsa su "richiesta".

Utilizzano l'API Cache per archiviare le risposte JSON:

Logo di 1-800 Flowers.
Precaricamento dei dati di prodotto JSON dalle pagine di elenco dei prodotti su 1-800Flowers.com.

Quando l'utente fa clic su un elemento, i dati JSON associati possono essere recuperati dalla cache, senza dover accedere alla rete, velocizzando la navigazione.

Utilizzare Workbox

Workbox offre un modo semplice per inviare messaggi a un worker di servizio tramite il pacchetto workbox-window, un insieme di moduli destinati a essere eseguiti nel contesto della finestra. Sono un complemento agli altri pacchetti Workbox che vengono eseguiti nel servizio worker.

Per comunicare la pagina con il service worker, ottieni prima un riferimento all'oggetto Workbox per il service worker registrato:

const wb = new Workbox('/sw.js');
wb.register();

A questo punto puoi inviare direttamente il messaggio in modo dichiarativo, senza dover eseguire la registrazione, verificare l'attivazione o preoccuparti dell'API di comunicazione sottostante:

wb.messageSW({"type": "PREFETCH", "payload": {"urls": ["/data1.json", "data2.json"]}}); });

Il service worker implementa un gestore message per ascoltare questi messaggi. Se vuoi, puoi restituire una risposta, anche se in casi come questi non è necessaria:

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PREFETCH') {
    // do something
  }
});

Utilizzo delle API del browser

Se la libreria Workbox non è sufficiente per le tue esigenze, ecco come puoi implementare la comunicazione tra la finestra e il worker del servizio utilizzando le API del browser.

L'API postMessage può essere utilizzata per stabilire un meccanismo di comunicazione unidirezionale dalla pagina al service worker.

La pagina chiama postMessage() sull'interfaccia del service worker:

navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
  payload: 'some data to perform the task',
});

Il service worker implementa un gestore message per ascoltare questi messaggi.

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === MSG_ID) {
    // do something
  }
});

L'attributo {type : 'MSG_ID'} non è assolutamente obbligatorio, ma è un modo per consentire alla pagina di inviare diversi tipi di istruzioni al service worker (ad es. "per eseguire il pre-caricamento" o "per svuotare lo spazio di archiviazione"). Il service worker può ramificarsi in diversi percorsi di esecuzione in base a questo flag.

Se l'operazione è andata a buon fine, l'utente potrà usufruirne, altrimenti il flusso principale dell'utente non verrà modificato. Ad esempio, quando 1-800-Flowers.com tenta di eseguire la memorizzazione nella cache, la pagina non ha bisogno di sapere se il servizio worker ha avuto esito positivo. In questo caso, l'utente potrà navigare più velocemente. In caso contrario, la pagina deve comunque passare alla nuova pagina. Ci vorrà solo un po' più di tempo.

Un semplice esempio di precaricamento

Una delle applicazioni più comuni della cache imperativa è il prefetching, ovvero il recupero delle risorse per un determinato URL prima che l'utente vi acceda, in modo da velocizzare la navigazione.

Esistono diversi modi per implementare il pre-caricamento nei siti:

Per scenari di precaricamento relativamente semplici, come il precaricamento di documenti o asset specifici (JS, CSS e così via), queste tecniche sono l'approccio migliore.

Se è richiesta una logica aggiuntiva, ad esempio l'analisi della risorsa di prefetch (una pagina o un file JSON) per recuperare i relativi URL interni, è più appropriato delegare completamente questa attività al servizio worker.

La delega di questi tipi di operazioni al service worker presenta i seguenti vantaggi:

  • Offload del lavoro più pesante di recupero ed elaborazione post-recupero (che verrà introdotto più avanti) su un thread secondario. In questo modo, il thread principale è libero di gestire attività più importanti come la risposta alle interazioni degli utenti.
  • Consentire a più client (ad es. schede) di riutilizzare una funzionalità comune e persino di chiamare il servizio contemporaneamente senza bloccare il thread principale.

Precaricare le pagine dei dettagli del prodotto

Innanzitutto, utilizza postMessage() nell'interfaccia del servizio worker e passa un array di URL da memorizzare nella cache:

navigator.serviceWorker.controller.postMessage({
  type: 'PREFETCH',
  payload: {
    urls: [
      'www.exmaple.com/apis/data_1.json',
      'www.exmaple.com/apis/data_2.json',
    ],
  },
});

Nel service worker, implementa un gestore message per intercettare ed elaborare i messaggi inviati da qualsiasi scheda attiva:

addEventListener('message', (event) => {
  let data = event.data;
  if (data && data.type === 'PREFETCH') {
    let urls = data.payload.urls;
    for (let i in urls) {
      fetchAsync(urls[i]);
    }
  }
});

Nel codice precedente abbiamo introdotto una piccola funzione di supporto chiamata fetchAsync() per eseguire l'iterazione sull'array di URL ed emettere una richiesta di recupero per ciascuno di essi:

async function fetchAsync(url) {
  // await response of fetch call
  let prefetched = await fetch(url);
  // (optionally) cache resources in the service worker storage
}

Una volta ottenuta la risposta, puoi fare affidamento sulle intestazioni della cache della risorsa. Tuttavia, in molti casi, come nelle pagine dei dettagli dei prodotti, le risorse non vengono memorizzate nella cache (ovvero hanno un Cache-control header di no-cache). In questi casi puoi ignorare questo comportamento memorizzando la risorsa recuperata nella cache del service worker. Questo ha il vantaggio aggiuntivo di consentire il caricamento del file in scenari offline.

Oltre i dati JSON

Una volta recuperati da un endpoint del server, i dati JSON spesso contengono altri URL che vale la pena prelevare, ad esempio un'immagine o altri dati endpoint associati a questi dati di primo livello.

Supponiamo che nel nostro esempio i dati JSON restituiti siano le informazioni di un sito di spesa alimentare:

{
  "productName": "banana",
  "productPic": "https://cdn.example.com/product_images/banana.jpeg",
  "unitPrice": "1.99"
 }

Modifica il codice fetchAsync() per eseguire l'iterazione sull'elenco dei prodotti e memorizzare nella cache l'immagine hero per ciascuno di essi:

async function fetchAsync(url, postProcess) {
  // await response of fetch call
  let prefetched = await fetch(url);

  //(optionally) cache resource in the service worker cache

  // carry out the post fetch process if supplied
  if (postProcess) {
    await postProcess(prefetched);
  }
}

async function postProcess(prefetched) {
  let productJson = await prefetched.json();
  if (productJson && productJson.product_pic) {
    fetchAsync(productJson.product_pic);
  }
}

Puoi aggiungere un po' di gestione delle eccezioni a questo codice per situazioni come gli errori 404. Ma il vantaggio dell'utilizzo di un service worker per il pre-caricamento è che può non riuscire senza conseguenze significative per la pagina e il thread principale. Potresti anche avere una logica più elaborata nell'elaborazione post-caricamento dei contenuti pre-recuperati, rendendoli più flessibili e disaccoppiati dai dati che gestiscono. Non ci sono limiti.

Conclusione

In questo articolo abbiamo trattato un caso d'uso comune della comunicazione unidirezionale tra la pagina e il worker di servizio: la cache imperativa. Gli esempi discussi hanno lo scopo di dimostrare solo un modo di utilizzare questo pattern e lo stesso approccio può essere applicato anche ad altri casi d'uso, ad esempio la memorizzazione nella cache degli articoli più popolari su richiesta per il consumo offline, i preferiti e altro ancora.

Per altri pattern di comunicazione tra pagine e worker di servizio, consulta:

  • Aggiornamenti di trasmissione: chiamata della pagina dal service worker per informare su aggiornamenti importanti (ad es. è disponibile una nuova versione dell'app web).
  • Comunicazione bidirezionale: delega di un'attività a un worker di servizio (ad es. un download impegnativo) e aggiornamento della pagina sullo stato di avanzamento.