In alcuni casi, un'app web potrebbe dover stabilire un canale di comunicazione bidirezionale tra la pagina e il service worker.
Ad esempio, in una PWA un podcast si potrebbe creare una funzionalità per consentire all'utente di scaricare puntate per il consumo offline e consentire al service worker di mantenere regolarmente la pagina informata dell'avanzamento, in modo che il thread principale possa aggiornare l'UI.
In questa guida esploreremo i diversi modi di implementare una comunicazione bidirezionale tra il contesto Window e Service worker, esplorando le diverse API, la libreria Workbox e alcuni casi avanzati.
Utilizzo di Workbox
workbox-window
è un insieme di moduli della libreria Workbox da eseguire nel contesto della finestra. La classe Workbox
fornisce un metodo messageSW()
per inviare un messaggio al service worker registrato dell'istanza e attendere una risposta.
Il seguente codice pagina crea una nuova istanza Workbox
e invia un messaggio al service worker per ottenerne la versione:
const wb = new Workbox('/sw.js');
wb.register();
const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);
Il service worker implementa un listener di messaggi dall'altro capo e risponde al service worker registrato:
const SW_VERSION = '1.0.0';
self.addEventListener('message', (event) => {
if (event.data.type === 'GET_VERSION') {
event.ports[0].postMessage(SW_VERSION);
}
});
In background, la libreria utilizza un'API del browser che esamineremo nella sezione successiva: Message Channel, ma astrae molti dettagli di implementazione, per facilitarne l'uso sfruttando il supporto ampio del browser di questa API.
Utilizzo delle API del browser
Se la libreria Workbox non è sufficiente per le tue esigenze, sono disponibili diverse API di livello inferiore per implementare la comunicazione "bidirezionale" tra le pagine e i service worker. Hanno alcune somiglianze e differenze:
Analogie:
- In tutti i casi, la comunicazione inizia da un'estremità tramite l'interfaccia
postMessage()
e viene ricevuta dall'altra parte mediante l'implementazione di un gestoremessage
. - In pratica, tutte le API disponibili ci consentono di implementare gli stessi casi d'uso, ma alcune potrebbero semplificare lo sviluppo in alcuni scenari.
Differenze:
- Hanno diversi modi per identificare l'altro lato della comunicazione: alcuni utilizzano un riferimento esplicito all'altro contesto, mentre altri possono comunicare implicitamente tramite un oggetto proxy creato su ciascun lato.
- Il supporto dei browser varia da uno all'altro.
API Broadcast Channel
L'API Broadcast Channel consente le comunicazioni di base tra i contesti di navigazione tramite gli oggetti BroadcastChannel.
Per implementarlo, in primo luogo ogni contesto deve creare un'istanza di un oggetto BroadcastChannel
con lo stesso ID e inviare e ricevere messaggi da questo oggetto:
const broadcast = new BroadcastChannel('channel-123');
L'oggetto BroadcastChannel espone un'interfaccia postMessage()
per inviare un messaggio a qualsiasi contesto di ascolto:
//send message
broadcast.postMessage({ type: 'MSG_ID', });
Qualsiasi contesto del browser può ascoltare i messaggi tramite il metodo onmessage
dell'oggetto BroadcastChannel
:
//listen to messages
broadcast.onmessage = (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//process message...
}
};
Come visto, non esiste un riferimento esplicito a un particolare contesto, quindi non è necessario ottenere prima un riferimento al service worker o a un cliente specifico.
Lo svantaggio è che, al momento della stesura di questo articolo, l'API supporta Chrome, Firefox ed Edge, ma altri browser, come Safari, non lo supportano ancora.
API client
L'API client consente di ottenere un riferimento a tutti gli oggetti WindowClient
che rappresentano le schede attive controllate dal service worker.
Poiché la pagina è controllata da un singolo service worker, la pagina ascolta e invia messaggi al service worker attivo direttamente tramite l'interfaccia serviceWorker
:
//send message
navigator.serviceWorker.controller.postMessage({
type: 'MSG_ID',
});
//listen to messages
navigator.serviceWorker.onmessage = (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//process response
}
};
Allo stesso modo, il service worker ascolta i messaggi implementando un listener onmessage
:
//listen to messages
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'MSG_ID') {
//Process message
}
});
Per comunicare con i suoi client, il service worker ottiene un array di oggetti WindowClient
eseguendo metodi come Clients.matchAll()
e Clients.get()
. Poi può postMessage()
:
//Obtain an array of Window client objects
self.clients.matchAll(options).then(function (clients) {
if (clients && clients.length) {
//Respond to last focused tab
clients[0].postMessage({type: 'MSG_ID'});
}
});
Client API
è una buona opzione per comunicare facilmente con tutte le schede attive di un service worker
in modo relativamente semplice. L'API è supportata da tutti i principali browser, ma non tutti i suoi metodi potrebbero essere disponibili, pertanto assicurati di verificare il supporto del browser prima di implementarlo nel tuo sito.
Canale messaggi
Il canale dei messaggi richiede la definizione e il passaggio di una porta da un contesto a un altro per stabilire un canale di comunicazione bidirezionale.
Per inizializzare il canale, la pagina crea un'istanza di un oggetto MessageChannel
e lo utilizza per inviare una porta al service worker registrato. La pagina implementa anche un listener onmessage
per ricevere messaggi dall'altro contesto:
const messageChannel = new MessageChannel();
//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
messageChannel.port2,
]);
//Listen to messages
messageChannel.port1.onmessage = (event) => {
// Process message
};
Il service worker riceve la porta, salva un riferimento e la utilizza per inviare un messaggio all'altro lato:
let communicationPort;
//Save reference to port
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'PORT_INITIALIZATION') {
communicationPort = event.ports[0];
}
});
//Send messages
communicationPort.postMessage({type: 'MSG_ID'});
MessageChannel
è attualmente supportato da tutti i principali browser.
API avanzate: sincronizzazione in background e recupero in background
In questa guida abbiamo esplorato i modi di implementare tecniche di comunicazione bidirezionale, per casi relativamente semplici, come il trasferimento di un messaggio stringa che descriva l'operazione da eseguire o un elenco di URL da memorizzare nella cache da un contesto all'altro. In questa sezione esploreremo due API per gestire scenari specifici: mancanza di connettività e download di lunga durata.
Sincronizzazione in background
Un'app di chat potrebbe voler fare in modo che i messaggi non vadano mai persi a causa di problemi di connettività. L'API Background Sync consente di posticipare le azioni da ripetere quando l'utente dispone di una connettività stabile. Ciò è utile per garantire che qualsiasi cosa l'utente voglia inviare venga effettivamente inviato.
Anziché l'interfaccia postMessage()
, la pagina registra un sync
:
navigator.serviceWorker.ready.then(function (swRegistration) {
return swRegistration.sync.register('myFirstSync');
});
Il service worker quindi rimane in ascolto dell'evento sync
per elaborare il messaggio:
self.addEventListener('sync', function (event) {
if (event.tag == 'myFirstSync') {
event.waitUntil(doSomeStuff());
}
});
La funzione doSomeStuff()
deve restituire una promessa che indica l'esito positivo o negativo di qualsiasi operazione che si stia cercando di eseguire. Se viene completata, la sincronizzazione è completa. Se l'operazione non riesce, verrà pianificato un nuovo tentativo di sincronizzazione. I nuovi tentativi di sincronizzazione, inoltre, attendono la connettività e utilizzano un backoff esponenziale.
Completata l'operazione, il service worker può comunicare con la pagina per aggiornare l'interfaccia utente, utilizzando una qualsiasi delle API di comunicazione esplorate in precedenza.
La Ricerca Google utilizza la sincronizzazione in background per mantenere le query non riuscite a causa di una scarsa connettività e riprovare in un secondo momento quando l'utente sarà online. Una volta eseguita l'operazione, comunicano il risultato all'utente tramite una notifica push web:
Recupero in background
Per parti di lavoro relativamente brevi come l'invio di un messaggio o un elenco di URL da memorizzare nella cache, le opzioni esplorate finora sono una buona scelta. Se l'attività richiede troppo tempo, il browser ucciderà il worker del servizio, altrimenti ciò potrebbe compromettere la privacy e la batteria dell'utente.
L'API Background Fetch consente di trasferire un'attività lunga a un service worker, ad esempio il download di film, podcast o livelli di un gioco.
Per comunicare con il service worker dalla pagina, utilizza backgroundFetch.fetch
anziché
postMessage()
:
navigator.serviceWorker.ready.then(async (swReg) => {
const bgFetch = await swReg.backgroundFetch.fetch(
'my-fetch',
['/ep-5.mp3', 'ep-5-artwork.jpg'],
{
title: 'Episode 5: Interesting things.',
icons: [
{
sizes: '300x300',
src: '/ep-5-icon.png',
type: 'image/png',
},
],
downloadTotal: 60 * 1024 * 1024,
},
);
});
L'oggetto BackgroundFetchRegistration
consente alla pagina di rimanere in ascolto dell'evento progress
per seguire
l'avanzamento del download:
bgFetch.addEventListener('progress', () => {
// If we didn't provide a total, we can't provide a %.
if (!bgFetch.downloadTotal) return;
const percent = Math.round(
(bgFetch.downloaded / bgFetch.downloadTotal) * 100,
);
console.log(`Download progress: ${percent}%`);
});
Passaggi successivi
In questa guida abbiamo esplorato il caso più generale di comunicazione tra page e service worker (comunicazione bidirezionale).
Molte volte, uno potrebbe aver bisogno di un solo contesto per comunicare con l'altro, senza ricevere una risposta. Consulta le seguenti guide per indicazioni su come implementare tecniche unidirezionali nelle tue pagine da e verso il service worker, oltre a casi d'uso ed esempi di produzione:
- Guida alla memorizzazione nella cache imperativa: chiamata a un service worker dalla pagina per memorizzare le risorse in anticipo (ad esempio, in scenari di precaricamento).
- Trasmetti aggiornamenti: chiamata alla pagina dal service worker per informare in merito ad aggiornamenti importanti (ad esempio è disponibile una nuova versione dell'app web).