Communication bidirectionnelle avec les service workers

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

Dans certains cas, une application Web peut être amenée à établir un canal de communication à deux voies entre la page et le service worker.

Par exemple, dans une PWA de podcast, vous pouvez créer une fonctionnalité permettant à l'utilisateur de télécharger des épisodes pour une consommation hors connexion et permettant au service worker de tenir la page régulièrement informée de la progression, afin que le thread principal puisse mettre à jour l'UI.

Dans ce guide, nous allons explorer les différentes manières d'implémenter une communication à deux voies entre le contexte Window et le contexte service worker, en explorant différentes API, la bibliothèque Workbox, ainsi que certains cas avancés.

Schéma montrant un service worker et la page échangeant des messages.

Utiliser Workbox

workbox-window est un ensemble de modules de la bibliothèque Workbox destinés à s'exécuter dans le contexte de la fenêtre. La classe Workbox fournit une méthode messageSW() pour envoyer un message au service worker enregistré de l'instance et attendre une réponse.

Le code de page suivant crée une instance Workbox et envoie un message au service worker pour obtenir sa version:

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

const swVersion = await wb.messageSW({type: 'GET_VERSION'});
console.log('Service Worker version:', swVersion);

Le service worker implémente un écouteur de messages à l'autre extrémité et répond au service worker enregistré:

const SW_VERSION = '1.0.0';

self.addEventListener('message', (event) => {
  if (event.data.type === 'GET_VERSION') {
    event.ports[0].postMessage(SW_VERSION);
  }
});

Sous le capot, la bibliothèque utilise une API de navigateur que nous examinerons dans la section suivante: Message Channel, mais elle s'abstrait de nombreux détails d'implémentation, ce qui la rend plus facile à utiliser, tout en exploitant la large compatibilité avec les navigateurs de cette API.

Schéma illustrant la communication bidirectionnelle entre la page et le service worker, à l'aide de la fenêtre Workbox.

Utiliser les API du navigateur

Si la bibliothèque Workbox ne répond pas à vos besoins, plusieurs API de niveau inférieur sont disponibles pour implémenter une communication "à deux sens" entre les pages et les services workers. Ils présentent certaines similitudes et différences:

Similitudes :

  • Dans tous les cas, la communication commence à une extrémité via l'interface postMessage() et est reçue à l'autre extrémité en implémentant un gestionnaire message.
  • En pratique, toutes les API disponibles nous permettent d'implémenter les mêmes cas d'utilisation, mais certaines d'entre elles peuvent simplifier le développement dans certains scénarios.

Différences :

  • Ils ont différentes façons d'identifier l'autre côté de la communication: certains utilisent une référence explicite à l'autre contexte, tandis que d'autres peuvent communiquer implicitement via un objet proxy instancié de chaque côté.
  • La compatibilité avec les navigateurs varie.
Schéma illustrant la communication bidirectionnelle entre la page et le service worker, ainsi que les API de navigateur disponibles.

API Broadcast Channel

Navigateurs pris en charge

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

Source

L'API Broadcast Channel permet une communication de base entre les contextes de navigation via des objets BroadcastChannel.

Pour l'implémenter, chaque contexte doit d'abord instancier un objet BroadcastChannel avec le même ID, puis envoyer et recevoir des messages à partir de celui-ci:

const broadcast = new BroadcastChannel('channel-123');

L'objet BroadcastChannel expose une interface postMessage() pour envoyer un message à n'importe quel contexte d'écoute:

//send message
broadcast.postMessage({ type: 'MSG_ID', });

Tout contexte de navigateur peut écouter les messages via la méthode onmessage de l'objet BroadcastChannel:

//listen to messages
broadcast.onmessage = (event) => {
  if (event.data && event.data.type === 'MSG_ID') {
    //process message...
  }
};

Comme vous pouvez le constater, il n'y a aucune référence explicite à un contexte particulier. Il n'est donc pas nécessaire d'obtenir d'abord une référence au service worker ou à un client particulier.

Schéma illustrant la communication bidirectionnelle entre la page et le service worker, à l'aide d'un objet Broadcast Channel.

L'inconvénient est qu'à l'heure où nous écrivons ces lignes, l'API est compatible avec Chrome, Firefox et Edge, mais que d'autres navigateurs, comme Safari, ne le sont pas encore.

API client

Navigateurs pris en charge

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

Source

L'API client vous permet d'obtenir une référence à tous les objets WindowClient représentant les onglets actifs que le service worker contrôle.

Étant donné que la page est contrôlée par un seul service worker, elle écoute et envoie des messages au service worker actif directement via l'interface 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
  }
};

De même, le service worker écoute les messages en implémentant un écouteur onmessage:

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

Pour communiquer avec l'un de ses clients, le service worker obtient un tableau d'objets WindowClient en exécutant des méthodes telles que Clients.matchAll() et Clients.get(). Il peut ensuite postMessage() l'un d'entre eux:

//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'});
  }
});
Schéma illustrant un service worker communiquant avec un tableau de clients.

Client API est une bonne option pour communiquer facilement avec tous les onglets actifs à partir d'un service worker de manière relativement simple. L'API est compatible avec tous les principaux navigateurs, mais toutes ses méthodes ne sont pas forcément disponibles. Veillez donc à vérifier la compatibilité du navigateur avant de l'implémenter sur votre site.

Canal de messagerie

Navigateurs pris en charge

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

Source

Message Channel nécessite de définir et de transmettre un port d'un contexte à un autre pour établir un canal de communication bidirectionnel.

Pour initialiser le canal, la page instancie un objet MessageChannel et l'utilise pour envoyer un port au service worker enregistré. La page implémente également un écouteur onmessage pour recevoir des messages de l'autre contexte:

const messageChannel = new MessageChannel();

//Init port
navigator.serviceWorker.controller.postMessage({type: 'PORT_INITIALIZATION'}, [
  messageChannel.port2,
]);

//Listen to messages
messageChannel.port1.onmessage = (event) => {
  // Process message
};
Schéma montrant une page transmettant un port à un service worker pour établir une communication bidirectionnelle.

Le service worker reçoit le port, enregistre une référence à celui-ci et l'utilise pour envoyer un message à l'autre côté:

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 est actuellement compatible avec tous les principaux navigateurs.

API avancées: synchronisation en arrière-plan et récupération en arrière-plan

Dans ce guide, nous avons exploré des moyens d'implémenter des techniques de communication à deux voies, pour des cas relativement simples, comme transmettre un message de chaîne décrivant l'opération à effectuer ou une liste d'URL à mettre en cache d'un contexte à l'autre. Dans cette section, nous allons explorer deux API pour gérer des scénarios spécifiques: l'absence de connectivité et les téléchargements longs.

Synchronisation en arrière-plan

Navigateurs pris en charge

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

Source

Une application de chat peut vouloir s'assurer que les messages ne sont jamais perdus en raison d'une mauvaise connectivité. L'API Background Sync vous permet de différer les actions à réessayer lorsque la connectivité de l'utilisateur est stable. Cela permet de s'assurer que tout ce que l'utilisateur souhaite envoyer est effectivement envoyé.

Au lieu de l'interface postMessage(), la page enregistre un sync:

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

Le service worker écoute ensuite l'événement sync pour traiter le message:

self.addEventListener('sync', function (event) {
  if (event.tag == 'myFirstSync') {
    event.waitUntil(doSomeStuff());
  }
});

La fonction doSomeStuff() doit renvoyer une promesse indiquant le succès ou l'échec de ce qu'elle tente de faire. Si elle est remplie, la synchronisation est terminée. En cas d'échec, une autre synchronisation sera planifiée pour réessayer. Les tentatives de synchronisation attendent également la connectivité et utilisent un intervalle exponentiel entre les tentatives.

Une fois l'opération effectuée, le service worker peut communiquer à nouveau avec la page pour mettre à jour l'UI, à l'aide de l'une des API de communication explorées précédemment.

La recherche Google utilise la synchronisation en arrière-plan pour conserver les requêtes ayant échoué en raison d'une mauvaise connectivité et les réessayer plus tard lorsque l'utilisateur est en ligne. Une fois l'opération effectuée, ils communiquent le résultat à l'utilisateur via une notification push Web:

Schéma montrant une page transmettant un port à un service worker pour établir une communication bidirectionnelle.

Récupération de l'arrière-plan

Navigateurs pris en charge

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

Source

Pour des tâches relativement courtes, comme l'envoi d'un message ou d'une liste d'URL à mettre en cache, les options explorées jusqu'à présent sont un bon choix. Si la tâche prend trop de temps, le navigateur arrête le worker de service, sinon il risque de compromettre la confidentialité et la batterie de l'utilisateur.

L'API Background Fetch vous permet de déléguer une tâche longue à un service worker, comme le téléchargement de films, de podcasts ou de niveaux d'un jeu.

Pour communiquer avec le service worker à partir de la page, utilisez backgroundFetch.fetch au lieu de 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'objet BackgroundFetchRegistration permet à la page d'écouter l'événement progress pour suivre la progression du téléchargement:

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}%`);
});
Schéma montrant une page transmettant un port à un service worker pour établir une communication bidirectionnelle.
L'UI est mise à jour pour indiquer la progression d'un téléchargement (à gauche). Grâce aux services workers, l'opération peut continuer à s'exécuter lorsque tous les onglets ont été fermés (à droite).

Étapes suivantes

Dans ce guide, nous avons exploré le cas le plus général de communication entre la page et les services workers (communication bidirectionnelle).

Souvent, un seul contexte peut suffire pour communiquer avec l'autre, sans recevoir de réponse. Consultez les guides suivants pour savoir comment implémenter des techniques unidirectionnelles dans vos pages depuis et vers le service worker, ainsi que des cas d'utilisation et des exemples de production:

  • Guide de mise en cache impérative: appel d'un service worker à partir de la page pour mettre en cache les ressources à l'avance (par exemple, dans les scénarios de préchargement).
  • Diffuser des mises à jour: appel de la page à partir du service worker pour informer des mises à jour importantes (par exemple, une nouvelle version de l'application Web est disponible).