Cache
Introduction
L’une des caractéristiques d’une application native réside dans sa capacité à être facilement utilisable lorsque l’utilisateur n’a pas de connexion. Cela ne veut pas dire que 100 % des fonctionnalités sont opérationnelles, mais au moins, comme première étape, l’utilisateur peut interagir avec l’application même quand il est dans le métro : il peut naviguer sur les différentes pages, éventuellement lire le contenu… Il en est ainsi quand l’utilisateur n’a pas du tout de réseau ou encore quand son téléphone indique qu’il a du réseau, mais pas de manière suffisante pour charger la page rapidement. Nous appelons ce phénomène le LI-FI.
Dans ce chapitre, nous allons aborder l’API Cache. Ce standard permet de mettre en cache les requêtes HTTP réalisées par l’application et de définir certaines stratégies pour indiquer où le navigateur doit récupérer la réponse HTTP associée.
Événements offline et online
Avant d’expliquer en détail cette API, une bonne pratique en termes d’expérience utilisateur doit être mise en place. Même si une application peut être utilisable hors connexion, elle ne l’est peut-être pas à 100 %, peut-être certaines fonctionnalités ne sont-elles pas disponibles. Afin d’éviter de dégrader l’expérience que peut avoir l’utilisateur avec l’application, il est recommandé d’ajouter visuellement sur le site un indicateur lorsque le statut de connexion de l’utilisateur change.
Pour cela, nous pouvons utiliser dans le code de l’application (donc pas dans celui du Service Worker), la propriété navigator.onLine permettant, au rafraîchissement de la page, de connaître l’état de la connectivité. Cette propriété retourne la valeur true lorsque l’utilisateur est connecté, false dans le cas contraire.
if(navigator.onLine) {
...
}
En outre, nous souhaitons être averti d’un changement au niveau de la connectivité : quand elle passe de « en ligne » à « hors ligne », et inversement. Pour cela, nous écoutons via la méthode addEventListener, déjà abordée précédemment...
API Network Information
En plus des événements définis dans la section précédente permettant d’être averti du changement de connectivité de l’utilisateur (de « en ligne » à « hors ligne » ou inversement), l’API Network Information permet de récupérer des informations sur la connexion, par exemple la bande passante ou le type de connexion utilisée. Libre à nous de charger du contenu multimédia d’une qualité optimisée en fonction du type de connexion.
Avant de l’utiliser, nous allons tout d’abord nous assurer que le navigateur la supporte en faisant une feature detection.
if (navigator.connection) {
...
} else {
console.log(‘Ce navigateur ne supporte pas cette API') ;
}
Puis, nous récupérons les informations afin de savoir si nous devons précharger une vidéo qui sera affichée sur notre page web.
if(navigator.connection){
if(connection.type === “wifi”){
preloadVideo();
}
}
Deux propriétés sont importantes dans cette API :
-
La propriété type qui indique le type de connexion utilisée. Elle peut retourner les valeurs...
API Cache
Nous allons à présent expliquer les différentes méthodes disponibles dans l’une des API primordiales des Progressive Web Apps : l’API Cache. Voici sa structure :
interface Cache {
add()
addAll()
match()
matchAll()
put()
delete()
keys()
}
1. Méthode add
La méthode add permet d’exécuter une requête et de mettre le résultat dans le cache. Le paramètre de cette méthode peut être soit une chaîne de caractères correspondant à une URL, soit un objet Request. L’API fait donc la requête et, une fois la réponse reçue, la met en cache.
cache.add(request);
2. Méthode addAll
Cette méthode ressemble à la méthode add, à part qu’elle permet de mettre en cache le résultat de plusieurs requêtes. Elle accepte donc soit un tableau de chaînes de caractères, soit des objets de type Request. L’API fait donc chaque requête et met ensuite en cache chaque réponse reçue.
cache.addAll([request1, request2]);
3. Méthode match
La méthode match permet de récupérer une entrée du cache. Si elle n’existe pas, la valeur retournée par la Promise est undefined. Le cache...
Où utiliser l’API Cache ?
Cette API est à la fois utilisable dans le Service Worker, cas d’utilisation qui sera présenté dans la section suivante, mais également dans l’application en utilisant la variable globale window.caches. Dans l’exemple ci-après, nous manipulons directement le cache dans le code principal de l’application afin d’y ajouter trois entrées.
caches.open('cache_name').then((cache) => {
cache.addAll(['/','/style.css','/script.js']) ;
})
.catch((err) => {
console.error(err) ;
})
Que ce soit dans le thread principal de l’application ou dans le Service Worker, il est nécessaire d’ouvrir le cache que nous souhaitons manipuler en utilisant la méthode open. En effet, il est tout à fait possible d’avoir différents caches pour une même application.
Avant d’utiliser cette API dans une application, il est nécessaire de tester tout d’abord le support par le navigateur pour respecter le côté progressif d’une Progressive Web App.
Dans cet exemple, nous vérifions que l’objet caches est défini sur l’objet window afin de manipuler cette API.
if('caches' in window) {
caches.open('cache_name').then((cache)...
Premières interaction avec l’API Cache
Comme première manipulation, nous allons interagir avec l’ API Cache depuis le code JavaScript de notre application. Nous définissons trois boutons dans l’interface graphique permettant :
-
d’ajouter une entrée dans le cache via l’utilisation de la méthode add
-
d’ajouter plusieurs entrées dans le cache via l’utilisation de la méthode addAll
-
de supprimer une entrée du cache via l’utilisation de la méthode delete
-
de supprimer plusieurs entrées du cache via l’utilisation des méthodes keys et delete
<button id="aadd">Ajouter plusieurs entrées</button>
<button id="add">Ajouter une entrée</button>
<button id="delete">Supprimer une entrée</button>
<button id="adelete">Supprimer toutes les entrées</button>
<script>
document.getElementById('aadd')
.addEventListener('click', () => {
if('caches' in window) {
window.caches.open('blog').then(cache => {
cache.addAll(['./posts/01_test', './posts/02_test2']);
});
} ...
Intégration dans les Service Workers
L’ API Cache peut à la fois être utilisée dans le thread principal du navigateur, mais également dans le Service Worker. Nous allons utiliser cette propriété pour implémenter un premier exemple d’utilisation de l’ API Cache afin de rendre notre application fonctionnelle indépendamment du réseau de l’utilisateur.
Nous allons mettre en place l’AppShell. L’AppShell est un terme correspondant aux ressources statiques nécessaires au navigateur pour construire le layout d’une page. Cela peut correspondre par exemple à la page HTML principale, à la feuille de style, à une partie du code JavaScript ou encore à certaines images, comme le logo du site qui est affiché sur une page d’accueil. Nous allons mettre en cache ces ressources statiques afin de donner l’impression à l’utilisateur que l’application se charge rapidement. Il s’agit bien ici de « donner l’impression », car si ensuite les autres requêtes nécessaires pour récupérer les données, par exemple, sont lentes, l’expérience utilisateur sera tout de même dégradée.
Nous allons utiliser deux événements présentés au chapitre Service Worker : fetch et install.
Durant l’événement...
Stratégies de mise en cache
Nous venons d’aborder le processus permettant de mettre en cache certaines ressources statiques d’une application. La stratégie que nous avons choisie dans les exemples précédents est la stratégie Cache falling back to network. Cette stratégie signifie que lorsqu’une requête est envoyée, nous vérifions tout d’abord si elle a déjà été mise dans le cache. Si ce n’est pas le cas, nous exécutons la requête HTTP.
Comme nous utilisons JavaScript pour implémenter notre système de mise en cache, nous pouvons en fait implémenter nos propres stratégies en fonction de ce que nous souhaitons.
Une amélioration que nous pouvons apporter aux exemples précédents est de mettre en cache le résultat de la requête HTTP, dans le but d’avoir la ressource hors connexion une prochaine fois. Pour cela, il suffit de mettre en cache la réponse reçue en ajoutant une méthode then à la Promise.
self.addEventListener('fetch', function(e) {
e.respondWith(
caches.open(cacheName)
.then(cache => cache.match(e.request))
.then(function(response) {
return response ||
fetch(e.request)
.then(resp=>...
Versionning du cache
Nous pouvons versionner le cache au fur et à mesure des versions de l’application. Ceci est nécessaire par exemple quand les ressources statiques ont été modifiées ou supprimées. Ce versionning est la plupart du temps géré automatiquement par des outils utilisés lors de la phase de build du projet (Webpack…).
Afin d’optimiser les données stockées côté client, il est important de supprimer les données précédemment mises dans le cache lorsque nous n’en avons plus besoin (version du cache différente de celle utilisée actuellement) ou lorsque l’utilisateur s’est déconnecté de l’application.
Pour la suppression des anciennes versions du cache, nous allons utiliser l’événement activate. Pour rappel, cet événement est émis lorsque le Service Worker est actif. Nous allons donc itérer sur l’ensemble des clés de l’objet cache. Chaque clé représente le nom d’un cache préalablement créé. Si le nom de l’un de ces caches est différent du cache utilisé actuellement (valeur de la variable cacheName), nous faisons la suppression.
self.addEventListener('activate', function(e) {
e.waitUntil(
caches.keys().then(function(keyList)...
Workbox
Pour un développeur web, écrire un Service Worker est assez facile, car cela ne nécessite que des connaissances JavaScript. Mais comme il est possible d’implémenter des stratégies différentes en fonction des requêtes, nous pouvons rapidement produire du code difficilement maintenable ou comportant de la duplication de code. Nous en avons eu un aperçu lors de l’implémentation de la stratégie Stale-while-revalidate.
Pour remédier à ce problème, nous disposons de la librairie Workbox développée par Google. Cette librairie propose des méthodes utilitaires permettant d’implémenter les stratégies de mise en cache présentées précédemment, en nous masquant toute la complexité d’écriture du code asynchrone. Cette librairie est déjà intégrée à de nombreux projets comme create-react-app, vue-cli, preact-cli ou encore Next.js.
Pour l’intégrer à notre Service Worker, il suffit tout d’abord de l’importer. Dans l’exemple ci-après, nous l’importons depuis un CDN (Content Delivery Network) en utilisant la méthode importScripts présentée dans le chapitre Service Worker.
importScripts('https://storage.googleapis.com/workbox-cdn/
releases/4.3.1/workbox-sw.js');
L’API...
API Content Indexing
Nous avons à présent les données disponibles hors connexion. Soit nous implémentons une interface graphique permettant de visualiser les données que nous avons en cache, soit nous pouvons utiliser une toute nouvelle API : Content Indexing. Cette solution permet d’indexer tous les éléments que nous avons mis en cache qui peuvent être manipulés et utilisés directement par les utilisateurs, par exemple des articles de notre blog ou encore des produits de notre site d’e-commerce.
Cette API permet d’ajouter certaines pages, que nous avons rendues utilisables sans connexion, dans un index qui sera créé et utilisé par le navigateur lui-même. Nous pouvons comparer cette fonctionnalité au site getpocket permettant de mettre en cache certains articles afin de pouvoir les lire plus tard.
Une fois l’article ajouté, le navigateur est en charge de l’affichage avec l’interface qui lui est propre. Chrome a décidé d’afficher ces éléments dans sa page downloads/Articles for you.
Cette API propose des méthodes permettant de faire trois types d’actions : ajouter, lister et supprimer des entrées.
{
add( config );
getAll();
delete(id)
}
Pour manipuler ces méthodes, nous allons utiliser...
Compatibilité navigateurs
Voici un tableau récapitulatif du support de l’ API Cache sur les principaux navigateurs du marché.
Si nous souhaitons implémenter une solution pour les navigateurs ne supportant pas cette API, nous pouvons utiliser une ancienne API (dépréciée en faveur de l’API Cache), qui se nomme AppCache. Cette API est notamment supportée par Internet Explorer et iOS Safari.
Pour utiliser cette API, nous devons implémenter un fichier dans lequel nous allons lister notre stratégie de mise en cache : quel contenu souhaitons-nous mettre en cache, quel contenu doit toujours être récupéré depuis le réseau… ?
Voici un exemple de fichier dans lequel nous listons trois ressources que nous souhaitons mettre en cache. Il est clair que les possibilités sont beaucoup plus limitées avec cette API qui ne permet pas d’utiliser le langage JavaScript.
CACHE MANIFEST
# v1
index.html
script.js
style.css
Une fois ce fichier créé, nous devons l’enregistrer en utilisant l’attribut manifest sur la balise head de nos pages.
<html manifest="example.appcache">
</html>
Cependant, ne faut-il pas craindre d’éventuels conflits si nous activons à la fois le fichier AppCache et le code utilisant l’API Cache ?...
Application fil rouge
Dans l’application fil rouge, nous allons mettre en cache les ressources statiques. Ces ressources sont au nombre de cinq : les fichiers index.html, script.js, la feuille de style de la librairie Bulma que nous utilisons, ainsi que les deux types d’images.
Tout d’abord, nous modifions la structure de notre Service Worker afin d’écouter les trois événements nécessaires. Nous définissons également une variable files pointant vers les trois fichiers à mettre en cache.
const cacheName = "blog-v1";
const files = [
"/",
"script.js",
"https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.css",
"https://bulma.io/images/placeholders/1280x960.png",
"https://bulma.io/images/placeholders/96x96.png"
];
self.addEventListener("install", event => {
});
self.addEventListener("activate", event => {
});
self.addEventListener("fetch", event => {
});
Durant l’événement install, nous mettons en cache toutes les ressources définies dans la variable files.
self.addEventListener("install", event => {
caches.open(cacheName).then(cache => {
cache.addAll(files);
});
});
Afin de prévoir les éventuelles nouvelles versions du cache, nous supprimons les anciennes versions pendant l’événement activate.
self.addEventListener("activate", event => {
event.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(
keyList.map(function(key) {
if (key !== cacheName) {
return caches.delete(key);
}
})
);
})
);
});
La dernière étape consiste à implémenter...