Comunicazione bidirezionale con i service worker

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

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 di podcast è possibile creare una funzionalità che consenta all'utente di scaricare le puntate per la fruizione offline e al servizio worker di aggiornare regolarmente la pagina sullo stato di avanzamento, in modo che il thread principale possa aggiornare l'interfaccia utente.

In questa guida esploreremo i diversi modi per implementare una comunicazione bidirezionale tra il contesto Window e service worker, esaminando diverse API, la libreria Workbox e alcuni casi avanzati.

Diagramma che mostra un service worker e la pagina che si scambiano messaggi.

Utilizzare Workbox

workbox-window è un insieme di moduli della libreria Workbox destinati a essere eseguiti 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 di Workbox e invia un messaggio al servizio worker per ottenere la relativa 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 ascoltatore di messaggi sull'altra estremità 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);
  }
});

Sotto il cofano, la libreria utilizza un'API del browser che esamineremo nella sezione successiva: Message Channel, ma esegue l'astrazione di molti dettagli di implementazione, semplificandone l'utilizzo, sfruttando al contempo l'ampio supporto dei browser di questa API.

Diagramma che mostra la comunicazione bidirezionale tra la pagina e il service worker, utilizzando la finestra Workbox.

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 pagine e 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 estremità implementando un gestore message.
  • In pratica, tutte le API disponibili ci consentono di implementare gli stessi casi d'uso, ma alcune di esse potrebbero semplificare lo sviluppo in alcuni scenari.

Differenze:

  • Hanno diversi modi per identificare l'altra parte della comunicazione: alcuni utilizzano un riferimento esplicito all'altro contesto, mentre altri possono comunicare in modo implicito tramite un oggetto proxy attivato su ogni lato.
  • Il supporto dei browser varia da una piattaforma all'altra.
Diagramma che mostra la comunicazione bidirezionale tra la pagina e il service worker e le API del browser disponibili.

API Broadcast Channel

Supporto dei browser

  • Chrome: 54.
  • Edge: 79.
  • Firefox: 38.
  • Safari: 15.4.

Origine

L'API Broadcast Channel consente la comunicazione di base tra i contesti di navigazione tramite gli oggetti BroadcastChannel.

Per implementarlo, innanzitutto ogni contesto deve creare un oggetto BroadcastChannel con lo stesso ID e inviare e ricevere messaggi da esso:

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 abbiamo visto, non esiste un riferimento esplicito a un determinato contesto, quindi non è necessario ottenere prima un riferimento al servizio worker o a un determinato client.

Diagramma che mostra la comunicazione bidirezionale tra la pagina e il service worker, utilizzando un oggetto Broadcast Channel.

Lo svantaggio è che, al momento della stesura di questo articolo, l'API è supportata da Chrome, Firefox e Edge, ma altri browser, come Safari, non la supportano ancora.

API client

Supporto dei browser

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1.

Origine

L'API client ti consente di ottenere un riferimento a tutti gli oggetti WindowClient che rappresentano le schede attive controllate dal servizio worker.

Poiché la pagina è controllata da un singolo service worker, 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
  }
};

Analogamente, il service worker ascolta i messaggi implementando un ascoltatore onmessage:

//listen to messages
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //Process message
  }
});

Per comunicare di nuovo con uno dei suoi client, il worker del servizio ottiene un array di oggetti WindowClient eseguendo metodi come Clients.matchAll() e Clients.get(). Quindi può postMessage() uno dei seguenti:

//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'});
  }
});
Diagramma che mostra un worker di servizio che comunica con una serie di client.

Client API è una buona opzione per comunicare facilmente con tutte le schede attive da un servizio worker in modo relativamente semplice. L'API è supportata da tutti i browser principali, ma non tutti i suoi metodi potrebbero essere disponibili, quindi assicurati di verificare il supporto del browser prima di implementarla nel tuo sito.

Canale messaggi

Supporto dei browser

  • Chrome: 2.
  • Edge: 12.
  • Firefox: 41.
  • Safari: 5.

Origine

Canale di messaggi richiede la definizione e il passaggio di una porta da un contesto all'altro per stabilire un canale di comunicazione bidirezionale.

Per inizializzare il canale, la pagina esegue l'inizializzazione di un oggetto MessageChannel e lo utilizza per inviare una porta al service worker registrato. La pagina implementa anche un ascoltatore 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
};
Diagramma che mostra una pagina che passa una porta a un worker di servizio per stabilire una comunicazione bidirezionale.

Il service worker riceve la porta, ne 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 per implementare tecniche di comunicazione bidirezionale per casi relativamente semplici, come il passaggio di un messaggio di stringa che descrive 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 lunghi.

Sincronizzazione in background

Supporto dei browser

  • Chrome: 49.
  • Edge: 79.
  • Firefox: non supportato.
  • Safari: non supportato.

Origine

Un'app di chat potrebbe voler assicurarsi che i messaggi non vengano mai persi a causa di una scarsa connettività. L'API Background Sync ti consente di posticipare le azioni da ripetere quando la connettività dell'utente è stabile. Questo è utile per assicurarti che qualunque cosa l'utente voglia inviare venga effettivamente inviata.

Invece dell'interfaccia postMessage(), la pagina registra un sync:

navigator.serviceWorker.ready.then(function (swRegistration) {
  return swRegistration.sync.register('myFirstSync');
});

Il service worker ascolta quindi l'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 indichi il successo/l'errore di qualsiasi operazione stia tentando di eseguire. Se viene soddisfatta, la sincronizzazione è completata. In caso di errore, verrà pianificata un'altra sincronizzazione per ritentare. Anche le sincronizzazioni con nuovi tentativi aspettano la connettività e utilizzano un backoff esponenziale.

Una volta eseguita l'operazione, il service worker può comunicare di nuovo con la pagina per aggiornare l'interfaccia utente utilizzando una 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 connettività scadente e riprovare più tardi quando l'utente è online. Una volta eseguita l'operazione, comunicano il risultato all'utente tramite una notifica push web:

Diagramma che mostra una pagina che passa una porta a un worker di servizio per stabilire una comunicazione bidirezionale.

Recupero in background

Supporto dei browser

  • Chrome: 74.
  • Edge: 79.
  • Firefox: non supportato.
  • Safari: non supportato.

Origine

Per attività relativamente brevi come l'invio di un messaggio o di un elenco di URL da memorizzare nella cache, le opzioni esplorate finora sono una buona scelta. Se l'attività richiede troppo tempo, il browser interromperà il servizio work, altrimenti rappresenta un rischio per la privacy e la batteria dell'utente.

L'API Background Fetch consente di scaricare un'attività lunga su un servizio 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 ascoltare l'evento progress per monitorare lo stato di 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}%`);
});
Diagramma che mostra una pagina che passa una porta a un worker di servizio per stabilire una comunicazione bidirezionale.
L'interfaccia utente viene aggiornata per indicare l'avanzamento di un download (a sinistra). Grazie ai worker di servizio, l'operazione può continuare a essere eseguita quando tutte le schede sono state chiuse (a destra).

Passaggi successivi

In questa guida abbiamo esplorato il caso più generale di comunicazione tra page e service worker (comunicazione bidirezionale).

Molte volte, un utente 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 dal e verso il service worker, oltre a casi d'uso ed esempi di produzione: