Développer sa première PWA 3/3

Suite et fin de cette série !

Enregistrer un Service Worker

Avant de commencer le développement de notre Service Worker, il va falloir l’enregistrer dans notre page d’accueil HTML. Pour ce faire, il suffit de demander au navigateur de s’en occuper dans un petit script JavaScript :

<script>
 if ('serviceWorker' in navigator) {
   window.addEventListener('load', () => {
     navigator.serviceWorker.register('service-worker.js')
       .then(registration => {
         console.log('😎 Le Service Worker est enregistré', registration);
       })
       .catch(error => {
         console.error('😥 L\'enregistrement ne s\'est pas bien passé :', error);
       });
   });
 }
</script>

La première ligne du script permet de vérifier si le navigateur gère les Services Workers, le support est aujourd’hui très bon mais il est important de ne pas générer d’erreur si le navigateur de l’utilisateur est trop ancien. Lorsqu’il est prêt, on demande au navigateur d’enregistrer le fichier qui contient le code de notre Service Worker. N’oubliez pas de créer ce fichier qui restera sans contenu pour le moment.

navigator.serviceWorker.register(...) renvoie une promesse avec un objet registration une fois que tout s’est bien passé, ou rejette une erreur s’il y a eu un souci.

Les DevTools de Chrome nous permettent de vérifier que le log d’enregistrement est bien présent et l’onglet Application > Service Workers qu’il s’est bien enregistré.

Le cycle de vie du Service Worker

Ajoutons ce bout de code dans le fichier service-worker.js :

self.addEventListener('install', event => {
  console.log('⤵️ Installation du Service Worker...');
});
 
self.addEventListener('activate', event => {
  console.log('🤖 Activation du Service worker...');
});
 
self.addEventListener('fetch', event => {
  console.log('🕸 Interception d\'un fetch vers :', event.request.url);
});

Après avoir rechargé la page, on peut constater dans nos logs que le Service Worker s’installe. À noter que les logs de l’enregistrement peuvent apparaître en dessous ou au-dessus car le Service Worker tourne dans un processus séparé et ne garantit donc pas qu’il sera exécuté avant ou après. En retournant dans l’onglet Application, on peut constater qu’une nouvelle version de notre Service Worker est disponible. Le navigateur ne l’active pas de lui-même pour éviter des mises à jour alors que le Service Worker est occupé, ici on peut cliquer sur skipWaiting pour le mettre à jour manuellement.

La nouvelle version de notre Service Worker en attente d’activation

Pour simplifier le développement, je vous conseille de cocher la case “Update on reload” qui mettra à jour notre Service Worker à chaque rechargement de page.

Le cycle de vie du Service Worker : installation puis activation

Une fois activé et si l’on recharge la page, on peut constater de nombreux logs d’interception de l’événement fetch. Cet événement est déclenché à chaque fois qu’une requête part de notre application. C’est ici que l’on va pouvoir récupérer ces requêtes et aiguiller la réponse soit pour réellement récupérer la ressource et la mettre en cache, soit pour servir ce qui est dans le cache.

Le cycle de vie complet du Service Worker lors de sa première installation

Mettre en cache les ressources pour fonctionner hors ligne

Il est temps de s’attaquer à la vraie valeur ajoutée de notre Service Worker : utilisons l’API Cache pour stocker nos ressources sur l’appareil de l’utilisateur ! Par ressource j’entends n’importe quel type de fichier qu’on peut habituellement récupérer sur une page web : fichier HTML, fichier CSS, image, police d’écriture… Ici nous utiliserons la stratégie de “Cache first” : on récupère en priorité depuis le cache, sinon depuis internet. Mais il est tout à fait possible d’implémenter d’autres stratégies de mise en cache.

const CURRENT_CACHE_NAME = 'v1';
 
...
 
self.addEventListener('fetch', event => {
 console.log('🕸 Interception d\'un fetch vers :', event.request.url);
 event.respondWith(caches.match(event.request)
   .then(cachedResponse => {
     if (cachedResponse) {
       console.log('💿 Réponse depuis le cache pour :', event.request.url);
       return cachedResponse;
     }
 
     return fetch(event.request).then(response => {
       if (!response || response.status !== 200 || response.type !== 'basic') {
         return response;
       }
 
       const responseToCache = response.clone();
       caches.open(CURRENT_CACHE_NAME).then(cache => {
         console.log('📲 Mise en cache de :', event.request.url);
         cache.put(event.request, responseToCache);
       });
 
       return response;
     });
   })
 );
});

La callback de l’événement fetch nous envoie un event qui nous permet de surcharger la réponse avec la méthode respondWith.

L’API Cache nous fournit un objet caches qui contient des méthodes pour stocker, récupérer et ouvrir le contenu d’un cache. Respectivement put, match, et open. On précise un nom de cache pour pouvoir l’invalider quand ça sera nécessaire.

La première partie du code est plutôt simple, on vérifie si un cache contient déjà la réponse associée à une requête, si c’est le cas : on la renvoie telle quelle.

Si le cache ne contient pas la réponse attendue, on lance la requête grâce à la méthode fetch (qui est disponible sur tous les navigateurs mobiles récents). Si la réponse est valide et provenant de notre domaine, on la stocke dans notre cache puis on la retourne.

Deux petites choses à noter :

  • On versionne notre cache avec CURRENT_CACHE_NAME. Si une ressource est mise à jour, on change la valeur de cette variable ce qui aura pour effet de rafraîchir toutes les réponses à l’activation du nouveau Service Worker.
  • La réponse étant un stream, il faut absolument la cloner pour pouvoir la renvoyer à la fin. Le contenu d’un stream ne peut être consommé qu’une seule fois !
Récupération des ressources depuis le Service Worker dans l’onglet Network des DevTools

Il est aussi tout à fait possible de filtrer les ressources qui nous intéressent et leur appliquer différentes stratégies de mise en cache.

Ne pas oublier de nettoyer derrière soi

Pour réellement faire fonctionner le versionning, il faut également penser à bien nettoyer les caches inactifs que l’on aurait pu laisser traîner. Voici comment on peut s’y prendre :

self.addEventListener('activate', event => {
 console.log('🤖 Activation du Service worker...');
 event.waitUntil(
   caches.keys().then(cacheNames => {
     return Promise.all(
       cacheNames.map(cacheName => {
         if (cacheName !== CURRENT_CACHE_NAME) {
           return caches.delete(cacheName);
         }
       })
     );
   })
 );
});

Les méthodes install et activate attendent qu’on leur donne la permission de se terminer avant de passer à la suite. Ici, je fournis une liste de promesses à exécuter avant de terminer l’activation : je parcours chaque cache et s’il est inutilisé, je le vide grâce à la méthode delete.

Stratégies de mise à jour

Si vous avez essayé de votre côté, la question qui doit venir maintenant est : comment est-ce que je mets réellement à jour mon application ? En effet, pour le moment nous trichons avec l’upload on reload. Le plus simple est de forcer l’activation d’un nouveau Service Worker directement après l’installation :

self.addEventListener('install', event => {
 console.log('⤵️ Installation du Service Worker...');
 self.skipWaiting();
});

Ainsi l’utilisateur récupérera la nouvelle version au prochain chargement de la page. Attention cependant car notre cache est vidé à l’activation, donc si l’utilisateur ne recharge pas la page, il n’y aura plus accès sans connexion ! Heureusement, il est possible d’écouter des événements sur l’objet registration et afficher un message à l’utilisateur pour qu’il recharge la page.

Enfin, nous aurons tendance à éviter ce genre de mises à jour si l’utilisateur ne souhaite pas être interrompu. Donc attention à bien réfléchir à la stratégie qui nous convient le mieux.

_____________

Grâce à tout ce travail, vous devriez obtenir de très bons scores Lighthouse PWA et PWABuilder !

Les scores Lighthouse de l’application de démo

Aller plus loin

Nous avons pu voir comment construire un Service Worker basique au plus bas niveau mais il existe de nombreux outils pour nous simplifier le travail. Le package npm register-service-worker améliore l’API serviceWorker en fournissant des callbacks sur les mises à jour de ressources ou le passage en offline par exemple. Workbox permet de faciliter le développement des différentes stratégies de mise en cache selon le type de fichiers et la durée souhaitée. PouchDB permet d’avoir une vraie expérience offline avec une file de messages à envoyer au serveur quand la connexion est récupérée.

Pour un framework utilisable directement, je vous conseille le projet open source bento-starter qui combine astucieusement Vue.js et Firebase pour se lancer rapidement avec une super expérience développeur.

Même si vous n’avez pas l’intérêt d’installer un site statique (comme celui de l’exemple) sur votre écran d’accueil, l’API Cache et les Service Workers améliorent grandement l’expérience de l’utilisateur en fluidifiant le rechargement après notre première visite.

Le futur des PWA

Alors que la part d’utilisation des ordinateurs de bureau diminue pour laisser place à la navigation mobile, le web se place comme un concurrent sérieux aux applications natives.

De nouvelles API web ne cessent d’apparaître pour s’approcher de l’expérience native : paiement en ligne, partage natif (web share), géolocalisation, communication temps réel WebRTC, presse-papier…

Certaines sont mêmes poussées par la communauté à travers le Project Fugu. On y trouve par exemple des APIs de détection de formes (visage ou texte) ou d’autorisation d’accès par empreinte digitale/reconnaissance faciale (WebAuthn) qui arrive tout juste avec iOS 14. D’ici quelques années, on pourra même se poser la question de l’intérêt des applications hybrides.

Côté plateformes, elles gèrent de mieux en mieux les PWA. Chrome et Microsoft Edge permettent de les installer sur les OS comme Windows, Linux ou MacOS. Certains OS les gèrent même nativement. On peut maintenant les distribuer sur le Play Store Android (on parle en fait de Trusted Web Activity). Il existe également une vitrine des meilleures expériences de PWA : AppScope.

Malheureusement, un acteur important du monde mobile ne joue pas le jeu : Apple. Alors que Steve Jobs était un des premiers promoteurs des applis mobile web (l’AppStore n’était pas initialement prévu), Apple refuse d’entendre parler de PWA leur préférant le nom d’HTML5 App ou d’Homescreen WebApp.

Steve Jobs présentant les applis Web à la WWDC 2007 avec le premier iPhone

Avec iOS 14 fraîchement sorti, les enthousiastes avaient des grosses attentes pour rattraper le retard : en particulier le support des notifications push et une meilleure expérience d’installation. Malheureusement il n’en est toujours rien, Apple fait faux bond et repousse le standard. Plus compréhensible, ils refusent d’implémenter des nouvelles APIs pour des raisons de respect de la vie privée : Bluetooth, NFC, niveau de batterie… Le support est amélioré chaque année mais vraiment au goutte-à-goutte.

J’en ai terminé pour cette intro au monde des PWA en 2020 que j’espère suffisamment concrète. Tout n’est pas rose mais le monde du Web a le mérite d’avoir un support assez incroyable même si tout n’y est pas encore disponible. Je suis persuadé que les PWA  auront une place importante dans le futur et je ne perds pas espoir pour que le standard regroupe enfin tous les acteurs. J’espère aussi vous avoir donné envie d’essayer les Service Workers pour profiter de vos sites sans connexion, et plus globalement de développer votre première PWA !


Sources


Nos formations Web


Une réflexion sur “Développer sa première PWA 3/3

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :