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 être informés du résultat. Voici quelques exemples :

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

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

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

Dans ce guide, nous allons découvrir 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 appellerons ces types de cas d'utilisation mise en cache impérative.

Demande de production

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

Logo de 1-800 Flowers.

Elle utilise une approche mixte pour décider des éléments à précharger:

  • Au moment du chargement de la page, elles demandent au servicer worker de récupérer les données JSON des neuf premiers éléments et d'ajouter les objets de réponse obtenus au cache.
  • Pour les éléments restants, ils écoutent l'événement mouseover . Ainsi, lorsqu'un utilisateur place le curseur sur un élément, il peut déclencher une extraction pour la ressource à la demande.

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

Logo de 1-800 Flowers.
Précharger les données produit JSON à partir des pages de fiches produit du site 1-800Flowers.com.

Lorsque l'utilisateur clique sur un élément, les données JSON associées peuvent être récupérées dans le cache, sans passer par le 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 d'une fenêtre. Ils complètent les autres packages Workbox qui s'exécutent 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 à vous soucier de l'enregistrement, de la vérification de l'activation ou de l'API de communication sous-jacente:

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

Le service worker met en œuvre 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 entre service worker à l'aide des API de navigateur.

L'API postMessage permet d'établir un mécanisme de communication unidirectionnel entre la page et 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 met en œuvre 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 requis, mais il s'agit d'un moyen d'autoriser la page à envoyer différents types d'instructions au service worker (par exemple, "précharger" ou "effacer le stockage"). Le service worker peut se connecter à différents chemins d'exécution en fonction de cet indicateur.

Si l'opération a réussi, l'utilisateur pourra en retirer les avantages, mais, si ce n'est pas le cas, cela ne modifiera pas le flux 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, l'utilisateur bénéficiera d'une navigation plus rapide. Si ce n'est pas le cas, la page doit quand même accéder à la nouvelle page. Cela va juste prendre un peu plus de temps.

Exemple de préchargement simple

L'une des applications les plus courantes de la mise en cache impérative est la préchargement, c'est-à-dire l'extraction des 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 manières de mettre en œuvre le préchargement sur les sites:

Pour les 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 constituent la meilleure approche.

Si une logique supplémentaire est nécessaire, par exemple, l'analyse de 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éguer ces types d'opérations au service worker présente les avantages suivants:

  • Décharger la charge lourde de l'extraction et du traitement de post-récupération (qui sera introduite plus tard) à un thread secondaire. Cela libère ainsi le thread principal pour qu'il puisse gérer des tâches plus importantes, telles que répondre aux interactions des utilisateurs.
  • Permettre à plusieurs clients (par exemple, des onglets) de réutiliser une fonctionnalité commune, voire appeler le service simultanément sans bloquer le thread principal.

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

Utilisez d'abord postMessage() dans l'interface du service worker, puis 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 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 compter sur les en-têtes de mise en cache de la ressource. Toutefois, dans de nombreux cas, comme dans les pages d'informations détaillées sur les produits, les ressources ne sont pas mises en cache (elles ont donc un en-tête Cache-control défini sur no-cache). Dans de tels cas, vous pouvez contourner ce comportement en stockant la ressource récupérée dans le cache du service worker. Cela présente l'avantage 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 méritent également 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 correspondent aux informations d'un site de vente alimentaire:

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

Modifiez le code fetchAsync() pour itérer la liste des produits et mettre en cache l'image héros de 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 dans des situations telles que les erreurs 404. Mais l'avantage du préchargement d'un service worker est qu'il peut échouer sans incidence majeure sur la page et le thread principal. Vous pouvez également appliquer 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 unidirectionnelle entre la page et le service worker: la mise en cache impérative. Les exemples abordés ne visent qu'à montrer 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 des meilleurs articles à la demande pour une consommation hors connexion, l'ajout de favoris, etc.

Pour découvrir d'autres modèles de communication entre page et service worker, consultez:

  • Diffuser les mises à jour: appel de la page depuis le service worker pour être informé des mises à jour importantes (par exemple, une nouvelle version de l'application Web est disponible).
  • Communication bidirectionnelle: déléguer une tâche à un service worker (un téléchargement volumineux, par exemple) et tenir la page informée de sa progression