Guide sur la mise en cache impérative

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

Certains sites Web peuvent avoir besoin de communiquer avec le service worker sans avoir besoin d'être informé du résultat. Voici quelques exemples :

  • Une page envoie au service worker une liste d'URL à précharger afin que, lorsque l'utilisateur clique sur un lien, les sous-ressources du document ou de la page soient déjà disponibles dans le cache, ce qui accélère considérablement la navigation ultérieure.
  • La page demande au service worker de récupérer et de mettre en cache un ensemble d'articles populaires afin de les rendre disponibles pour une utilisation hors connexion.

Délégation de ces types de tâches non critiques au service worker présente l'avantage de libérer le thread principal pour mieux gérer les tâches plus urgentes, telles que la réponse aux interactions utilisateur.

Schéma d'une page qui demande des ressources à mettre en cache à un service worker.

Dans ce guide, nous allons voir comment mettre en œuvre une technique de communication unidirectionnelle entre la page et le service worker à l'aide des API de navigateur standards et de la bibliothèque Workbox. Nous appelons ces types de cas d'utilisation mise en cache impérative.

Cas de production

1-800-Flowers.com a implémenté la mise en cache impérative (préchargement) avec des service workers via postMessage() pour précharger les principaux articles des pages de catégorie afin d'accélérer la navigation vers les pages d'informations détaillées sur les produits.

Logo de 1-800 Flowers.

Elles utilisent une approche mixte pour déterminer les éléments à précharger:

  • Au moment du chargement de la page, ils demandent au worker du servicer de récupérer les données JSON pour les neuf premiers éléments et d'ajouter les objets de réponse obtenus au cache.
  • Pour les autres éléments, ils écoutent l'événement mouseover afin que, lorsqu'un utilisateur déplace le curseur au-dessus d'un élément, il puisse déclencher une récupération de la ressource à la demande.

Ils utilisent l'API Cache pour stocker les réponses JSON:

Logo de 1-800 Flowers.
Préchargement des données produit JSON à partir des pages de fiches produit sur 1-800Flowers.com.

Lorsque l'utilisateur clique sur un élément, les données JSON qui lui sont associées peuvent être récupérées à partir du cache, sans avoir à accéder au réseau, ce qui accélère la navigation.

Utiliser Workbox

Workbox permet d'envoyer facilement des messages à un service worker via le package workbox-window, un ensemble de modules destinés à s'exécuter dans le contexte de la fenêtre. Ils complètent les autres packages Workbox exécutés dans le service worker.

Pour communiquer la page avec le service worker, commencez par obtenir une référence d'objet Workbox au service worker enregistré:

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

Vous pouvez ensuite envoyer directement le message de manière déclarative, sans avoir à obtenir l'enregistrement, à vérifier l'activation ni à penser à l'API de communication sous-jacente:

wb.messageSW({"type": "PREFETCH", "payload": {"urls": ["/data1.json", "data2.json"]}}); });

Le service worker implémente un gestionnaire message pour écouter ces messages. Elle peut éventuellement renvoyer une réponse, mais dans de tels cas, ce n'est pas nécessaire:

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'PREFETCH') {
    // do something
  }
});

Utiliser les API du navigateur

Si la bibliothèque Workbox ne répond pas à vos besoins, voici comment implémenter la communication de la fenêtre vers le service worker à l'aide des API du navigateur.

L'API postMessage peut être utilisée pour établir un mécanisme de communication à sens unique de la page vers le service worker.

La page appelle postMessage() sur l'interface du service worker:

navigator.serviceWorker.controller.postMessage({
  type: 'MSG_ID',
  payload: 'some data to perform the task',
});

Le service worker implémente un gestionnaire message pour écouter ces messages.

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === MSG_ID) {
    // do something
  }
});

L'attribut {type : 'MSG_ID'} n'est pas absolument obligatoire, mais il permet à la page d'envoyer différents types d'instructions au service worker (c'est-à-dire "à précharger" par rapport à "à effacer l'espace de stockage"). En fonction de cet indicateur, le service worker peut se ramifier dans différents chemins d'exécution.

Si l'opération a réussi, l'utilisateur pourra en profiter, mais si ce n'est pas le cas, cela n'affectera pas le parcours utilisateur principal. Par exemple, lorsque 1-800-Flowers.com tente d'effectuer une mise en pré-cache, la page n'a pas besoin de savoir si le service worker a réussi. Si c'est le cas, la navigation sera plus rapide. Si ce n'est pas le cas, la page doit quand même accéder à la nouvelle page. Cela va prendre un peu plus de temps.

Exemple simple de préchargement

L'une des applications les plus courantes du mise en cache impérative est le préchargement, qui consiste à extraire les ressources d'une URL donnée avant que l'utilisateur n'y accède, afin d'accélérer la navigation.

Il existe différentes façons d'implémenter le préchargement sur les sites:

Pour des scénarios de préchargement relativement simples, tels que le préchargement de documents ou d'éléments spécifiques (JS, CSS, etc.), ces techniques sont la meilleure approche.

Si une logique supplémentaire est requise, par exemple pour analyser la ressource de préchargement (un fichier ou une page JSON) afin d'extraire ses URL internes, il est plus approprié de déléguer entièrement cette tâche au service worker.

Délégation de ces types d'opérations au service worker présente les avantages suivants:

  • Décharger la tâche lourde de récupération et de post-traitement (qui sera présentée plus tard) sur un thread secondaire. De cette façon, il libère le thread principal pour qu'il gère des tâches plus importantes, telles que la réponse aux interactions utilisateur.
  • Permettre à plusieurs clients (par exemple, des onglets) de réutiliser une fonctionnalité commune, et même d'appeler le service simultanément sans bloquer le thread principal.

Précharger les pages d'informations détaillées sur les produits

Commencez par utiliser postMessage() sur l'interface du service worker et transmettez un tableau d'URL à mettre en cache:

navigator.serviceWorker.controller.postMessage({
  type: 'PREFETCH',
  payload: {
    urls: [
      'www.exmaple.com/apis/data_1.json',
      'www.exmaple.com/apis/data_2.json',
    ],
  },
});

Dans le service worker, implémentez un gestionnaire message pour intercepter et traiter les messages envoyés par n'importe quel onglet actif:

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]);
    }
  }
});

Dans le code précédent, nous avons introduit une petite fonction d'assistance appelée fetchAsync() pour itérer sur le tableau d'URL et émettre une requête de récupération pour chacune d'elles:

async function fetchAsync(url) {
  // await response of fetch call
  let prefetched = await fetch(url);
  // (optionally) cache resources in the service worker storage
}

Une fois la réponse obtenue, vous pouvez vous appuyer sur les en-têtes de mise en cache de la ressource. Dans de nombreux cas, comme dans les pages d'informations détaillées sur les produits, les ressources ne sont pas mises en cache (ce qui signifie qu'elles ont un en-tête Cache-control défini sur no-cache). Dans de tels cas, vous pouvez ignorer ce comportement en stockant la ressource récupérée dans le cache du service worker. Cela présente l'avantage supplémentaire de permettre la diffusion du fichier hors connexion.

Au-delà des données JSON

Une fois que les données JSON sont extraites d'un point de terminaison de serveur, elles contiennent souvent d'autres URL qui valent également la peine d'être préchargées, telles qu'une image ou d'autres données de point de terminaison associées à ces données de premier niveau.

Imaginons que, dans notre exemple, les données JSON renvoyées soient les informations d'un site d'épicerie:

{
  "productName": "banana",
  "productPic": "https://cdn.example.com/product_images/banana.jpeg",
  "unitPrice": "1.99"
 }

Modifiez le code fetchAsync() pour itérer sur la liste des produits et mettre en cache l'image héros pour chacun d'eux:

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);
  }
}

Vous pouvez ajouter une gestion des exceptions autour de ce code pour les situations telles que les erreurs 404. Mais l'avantage d'utiliser un service worker pour le préchargement est qu'il peut échouer sans trop de conséquences pour la page et le thread principal. Vous pouvez également avoir une logique plus élaborée pour le post-traitement du contenu préchargé, ce qui le rend plus flexible et découplé avec les données qu'il traite. Il n'y a pas de limite.

Conclusion

Dans cet article, nous avons abordé un cas d'utilisation courant de communication à sens unique entre page et service worker: la mise en cache impérative. Les exemples présentés ne visent qu'à illustrer une façon d'utiliser ce modèle. La même approche peut également être appliquée à d'autres cas d'utilisation, par exemple la mise en cache à la demande des articles les plus populaires pour une consommation hors connexion, l'ajout de favoris, etc.

Pour en savoir plus sur les modèles de communication entre les pages et les service workers, consultez les ressources suivantes:

  • 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).
  • Communication bidirectionnelle : délégation d'une tâche à un service worker (par exemple, un téléchargement lourd) et mise à jour de la page sur la progression.