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.
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.
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:
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 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:
- Utilizzo di tag di prefetch dei link nelle pagine: le risorse vengono conservate nella cache del browser per cinque minuti, dopodiché vengono applicate le normali regole
Cache-Control
per la risorsa. - Integrare la tecnica precedente con una strategia di memorizzazione nella cache in fase di esecuzione nel worker del servizio per estendere la durata della risorsa di prefetch oltre questo limite.
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.