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 sono già disponibili nella cache, rendendo la navigazione successiva molto più velocemente.
  • 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 attività più urgenti come la risposta alle interazioni degli utenti.

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 prelevare:

  • Al momento del caricamento della pagina, chiede al worker del fornitore di servizi di recuperare i dati JSON per i primi 9 articoli 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 fiori.
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.

Utilizzo di Workbox

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

Per comunicare la pagina al service worker, devi prima ottenere un riferimento a un oggetto Workbox al service worker registrato:

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

Quindi puoi inviare direttamente il messaggio in modo dichiarativo, senza preoccuparti di ottenere la registrazione, controllare l'attivazione o pensare all'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. Può facoltativamente restituire una risposta, anche se, in casi come questi, non è necessario:

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 (ovvero per "precaricare" invece di "per cancellare 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à usufruire di una navigazione più veloce. 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 precaricamento 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.

Precarica le pagine dei dettagli dei prodotti

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. In molti casi, però, come nelle pagine dei dettagli dei prodotti, le risorse non vengono memorizzate nella cache, il che significa che hanno un'intestazione Cache-control di no-cache. In casi come questi 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. Tuttavia, l'aspetto positivo dell'utilizzo di un service worker per il precaricamento è che l'operazione può avere esito negativo senza molte conseguenze 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:

  • Trasmetti aggiornamenti: chiamata alla 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.