Construire une PWA avec Webpack et Workbox

Publié: 2022-03-10
Résumé rapide ↬ Ce didacticiel vous aidera à transformer une application qui ne fonctionne pas hors ligne en une PWA qui fonctionne hors ligne et affiche une icône de mise à jour disponible. Vous apprendrez à mettre en précache les actifs avec workbox, à gérer la mise en cache dynamique ainsi qu'à gérer les mises à jour de votre PWA. Suivez-nous et voyez comment vous pouvez également appliquer ces techniques sur votre site Web.

Une application Web progressive (PWA) est un site qui utilise une technologie moderne pour offrir des expériences de type application sur le Web. C'est un terme générique pour les nouvelles technologies telles que le "manifeste d'application Web", le "travailleur de service", etc. Lorsqu'elles sont associées, ces technologies vous permettent d'offrir des expériences utilisateur rapides et attrayantes avec votre site Web.

Cet article est un didacticiel étape par étape pour ajouter un agent de service à un site Web d'une page existant. Le technicien de service vous permettra de faire fonctionner votre site Web hors ligne tout en informant vos utilisateurs des mises à jour de votre site. Veuillez noter que ceci est basé sur un petit projet fourni avec Webpack, nous utiliserons donc le plug-in Workbox Webpack (Workbox v4).

L'utilisation d'un outil pour générer votre service worker est l'approche recommandée car elle vous permet de gérer efficacement votre cache. Nous utiliserons Workbox - un ensemble de bibliothèques qui facilitent la génération de votre code de service worker - pour générer notre service worker dans ce didacticiel.

Selon votre projet, vous pouvez utiliser Workbox de trois manières différentes :

  1. Une interface de ligne de commande est disponible, ce qui vous permet d'intégrer Workbox dans n'importe quelle application dont vous disposez ;
  2. Un module Node.js est disponible qui vous permet d'intégrer la boîte de travail dans n'importe quel outil de construction de nœud tel que gulp ou grunt ;
  3. Un plugin Webpack est disponible qui vous permet de vous intégrer facilement à un projet construit avec Webpack.

Webpack est un bundler de modules. Pour simplifier, vous pouvez le considérer comme un outil qui gère vos dépendances JavaScript. Il vous permet d'importer du code JavaScript à partir de bibliothèques et de regrouper votre JavaScript dans un ou plusieurs fichiers.

Plus après saut! Continuez à lire ci-dessous ↓

Pour commencer, clonez le dépôt suivant sur votre ordinateur :

 git clone [email protected]:jadjoubran/workbox-tutorial-v4.git cd workbox-tutorial-v4 npm install npm run dev

Ensuite, accédez à https://localhost:8080/ . Vous devriez pouvoir voir l'application de devise que nous utiliserons tout au long de ce didacticiel :

Capture d'écran de l'application de devise que nous construisons dans cet article.
'Currencies' est une PWA qui répertorie les frais de conversion des devises par rapport à la devise Euro (€). ( Grand aperçu )

Commencez avec un shell d'application

Le shell d'application (ou 'app shell') est un pattern inspiré des Native Apps. Cela aidera à donner à votre application un aspect plus natif. Il fournit simplement à l'application une mise en page et une structure sans aucune donnée - un écran de transition visant à améliorer l'expérience de chargement de votre application Web.

Voici quelques exemples de shells d'application issus d'applications natives :

Shell d'application Google Inbox
Google Inbox App Shell : quelques millisecondes avant le chargement des e-mails dans le shell de l'application. ( Grand aperçu )
Shell d'application native Twitter
Application native de Twitter sur Android : shell d'application affichant la barre de navigation, les onglets et le chargeur. ( Grand aperçu )

Et voici des exemples de shells d'application de PWA :

App Shell de la PWA de Twitter
Le shell d'application de la PWA de Twitter ( Grand aperçu )
App Shell de la PWA de Flipkart
Le shell d'application de la PWA de Flipkart ( Grand aperçu )

Les utilisateurs apprécient l'expérience de chargement des shells d'application car ils méprisent les écrans vides. Un écran vide donne l'impression à l'utilisateur que le site Web ne se charge pas. Cela leur donne l'impression que le site Web était bloqué.

Les shells d'application tentent de peindre la structure de navigation de l'application dès que possible, comme la barre de navigation, la barre d'onglets ainsi qu'un chargeur indiquant que le contenu que vous avez demandé est en cours de chargement.

Alors, comment créez-vous un shell d'application ?

Le modèle de shell d'application donne la priorité au chargement du code HTML, CSS et JavaScript qui s'affichera en premier. Cela signifie que nous devons donner à ces ressources la priorité absolue, vous devez donc intégrer ces actifs. Donc, pour créer un shell d'application, il vous suffit d'intégrer le HTML, le CSS et le JavaScript qui sont responsables du shell de l'application. Bien sûr, vous ne devez pas tout mettre en ligne, mais plutôt rester dans une limite d'environ 30 à 40 Ko au total.

Vous pouvez voir le shell d'application intégré dans le fichier index.html . Vous pouvez inspecter le code source en consultant le fichier index.html et vous pouvez le prévisualiser dans le navigateur en supprimant l'élément <main> dans les outils de développement :

Devises PWA App Shell avec barre de navigation et chargeur
L'App Shell que nous construisons dans cet article. ( Grand aperçu )

Fonctionne-t-il hors ligne ?

Simulons une mise hors ligne ! Ouvrez DevTools, accédez à l'onglet réseau et cochez la case "Hors ligne". Lorsque vous rechargez la page, vous verrez que nous obtiendrons la page hors ligne du navigateur.

Page d'erreur hors ligne du navigateur
La demande à la page d'accueil a échoué, nous voyons donc cela comme un résultat. ( Grand aperçu )

En effet, la requête initiale à / (qui chargera le fichier index.html ) échouera car Internet est hors ligne. La seule façon pour nous de récupérer de cet échec de demande est d'avoir un agent de service.

Visualisons la requête sans service worker :

Animation d'une requête réseau allant du navigateur du client vers Internet.
La demande de réseau va du navigateur à Internet et inversement. (Icônes de flaticon.com) ( Grand aperçu )

Un agent de service est un proxy réseau programmable, ce qui signifie qu'il se situe entre votre page Web et Internet. Cela vous permet de contrôler les requêtes réseau entrantes et sortantes.

Animation d'une requête réseau interceptée par le service worker.
La requête réseau est interceptée par le service worker. (Icônes de flaticon.com) ( Grand aperçu )

Ceci est avantageux car nous pouvons maintenant rediriger cette demande échouée vers le cache (en supposant que nous ayons le contenu dans le cache).

Animation d'une requête réseau interceptée par le service worker et redirigée vers le cache.
La requête réseau est redirigée vers le cache lorsqu'elle existe déjà dans le cache. (Icônes de flaticon.com) ( Grand aperçu )

Un service worker est également un type de Web Worker, ce qui signifie qu'il s'exécute séparément de votre page principale et n'a accès ni à la window ni à l'objet document .

Précacher le shell de l'application

Afin de faire fonctionner notre application hors ligne, nous allons commencer par mettre en cache le shell de l'application.

Commençons donc par installer le plugin Webpack Workbox :

 npm install --save-dev workbox-webpack-plugin

Ensuite, nous allons ouvrir notre fichier index.js et enregistrer le service worker :

 if ("serviceWorker" in navigator){ window.addEventListener("load", () => { navigator.serviceWorker.register("/sw.js"); }) }

Ensuite, ouvrez le fichier webpack.config.js et configurons le plugin Webpack Workbox :

 //add at the top const WorkboxWebpackPlugin = require("workbox-webpack-plugin"); //add inside the plugins array: plugins: [ … , new WorkboxWebpackPlugin.InjectManifest({ swSrc: "./src/src-sw.js", swDest: "sw.js" }) ]

Cela demandera à Workbox d'utiliser notre fichier ./src/src-sw.js comme base. Le fichier généré s'appellera sw.js et se trouvera dans le dossier dist .

Créez ensuite un fichier ./src/src-sw.js au niveau racine et écrivez ce qui suit à l'intérieur :

 workbox.precaching.precacheAndRoute(self.__precacheManifest);

Note : La variable self.__precacheManifest sera importée depuis un fichier qui sera généré dynamiquement par workbox.

Vous êtes maintenant prêt à créer votre code avec npm run build et Workbox générera deux fichiers dans le dossier dist :

  • precache-manifest.66cf63077c7e4a70ba741ee9e6a8da29.js
  • sw.js

Le sw.js importe la boîte de travail du CDN ainsi que le manifeste de précache.[chunkhash].js .

 //precache-manifest.[chunkhash].js file self.__precacheManifest = (self.__precacheManifest || []).concat([ "revision": "ba8f7488757693a5a5b1e712ac29cc28", "url": "index.html" }, "url": "main.49467c51ac5e0cb2b58e.js" ]);

Le manifeste de précache répertorie les noms des fichiers qui ont été traités par webpack et qui se retrouvent dans votre dossier dist . Nous utiliserons ces fichiers pour les pré-cacher dans le navigateur. Cela signifie que lorsque votre site Web se charge pour la première fois et enregistre le service worker, il met en cache ces actifs afin qu'ils puissent être utilisés la prochaine fois.

Vous pouvez également remarquer que certaines entrées ont une "révision" alors que d'autres n'en ont pas. C'est parce que la révision peut parfois être déduite du chunkhash du nom de fichier. Par exemple, examinons de plus près le nom de fichier main.49467c51ac5e0cb2b58e.js . Il a une révision dans le nom de fichier, qui est le chunkhash 49467c51ac5e0cb2b58e .

Cela permet à Workbox de comprendre quand vos fichiers changent afin qu'il ne nettoie ou ne mette à jour que les fichiers qui ont été modifiés, plutôt que de vider tout le cache chaque fois que vous publiez une nouvelle version de votre service worker.

La première fois que vous chargez la page, le service worker s'installe. Vous pouvez le voir dans DevTools. Tout d'abord, le fichier sw.js est demandé, qui demande ensuite tous les autres fichiers. Ils sont clairement marqués avec l'icône d'engrenage.

Capture d'écran de l'onglet Réseau de DevTools lors de l'installation du service worker.
Les demandes marquées de l'icône ️ sont des demandes initiées par le service worker. ( Grand aperçu )

Ainsi, Workbox s'initialisera et mettra en précache tous les fichiers qui se trouvent dans le manifeste de précache. Il est important de vérifier que vous n'avez pas de fichiers inutiles dans le fichier manifeste de précache, tels que des fichiers .map ou des fichiers qui ne font pas partie du shell de l'application.

Dans l'onglet réseau, nous pouvons voir les requêtes provenant du service worker. Et maintenant, si vous essayez de vous déconnecter, le shell de l'application est déjà pré-caché, donc cela fonctionne même si nous sommes hors ligne !

Capture d'écran de l'onglet Réseau des outils de développement montrant l'échec des appels d'API.
Les appels d'API échouent lorsque nous nous déconnectons. ( Grand aperçu )

Cacher les routes dynamiques

Avez-vous remarqué que lorsque nous nous sommes déconnectés, le shell de l'application fonctionne, mais pas nos données ? En effet, ces appels d'API ne font pas partie du shell d'application pré -caché. Lorsqu'il n'y a pas de connexion Internet, ces demandes échouent et l'utilisateur ne peut pas voir les informations sur la devise.

Cependant, ces requêtes ne peuvent pas être pré-cachées car leur valeur provient d'une API. De plus, lorsque vous commencez à avoir plusieurs pages, vous ne voulez pas mettre en cache toutes les requêtes API en une seule fois. Au lieu de cela, vous souhaitez les mettre en cache lorsque l'utilisateur visite cette page.

Nous appelons cela des « données dynamiques ». Ils incluent souvent des appels d'API ainsi que des images et d'autres actifs qui sont demandés lorsqu'un utilisateur effectue une certaine action sur votre site Web (par exemple, lorsqu'il navigue vers une nouvelle page).

Vous pouvez les mettre en cache à l'aide du module de routage de Workbox. Voici comment:

 //add in src/src-sw.js workbox.routing.registerRoute( /https:\/\/api\.exchangeratesapi\.io\/latest/, new workbox.strategies.NetworkFirst({ cacheName: "currencies", plugins: [ new workbox.expiration.Plugin({ maxAgeSeconds: 10 * 60 // 10 minutes }) ] }) );

Cela configurera la mise en cache dynamique pour toute URL de demande qui correspond à l'URL https://api.exchangeratesapi.io/latest .

La stratégie de mise en cache que nous avons utilisée ici s'appelle NetworkFirst ; il y en a deux autres qui sont souvent utilisés :

  1. CacheFirst
  2. StaleWhileRevalidate

CacheFirst le cherchera d'abord dans le cache. S'il n'est pas trouvé, il l'obtiendra du réseau. StaleWhileRevalidate ira au réseau et au cache en même temps. Renvoyez la réponse du cache à la page (en arrière-plan), il utilisera la nouvelle réponse réseau pour mettre à jour le cache pour la prochaine fois qu'il sera utilisé.

Pour notre cas d'utilisation, nous avons dû opter pour NetworkFirst car nous avons affaire à des taux de change qui changent très souvent. Cependant, lorsque l'utilisateur se déconnecte, nous pouvons au moins lui montrer les taux tels qu'ils étaient il y a 10 minutes - c'est pourquoi nous avons utilisé le plugin d'expiration avec le maxAgeSeconds défini sur 10 * 60 secondes.

Gérer les mises à jour de l'application

Chaque fois qu'un utilisateur charge votre page, le navigateur exécute le code navigator.serviceWorker.register même si le service worker est déjà installé et en cours d'exécution. Cela permet au navigateur de détecter s'il existe une nouvelle version du service worker. Lorsque le navigateur remarque que le fichier n'a pas changé, il ignore simplement l'appel d'enregistrement. Une fois que ce fichier a été modifié, le navigateur comprend qu'il existe une nouvelle version du service worker, il installe donc le nouveau service worker parallèlement au service worker en cours d'exécution .

Cependant, il s'interrompt lors de la phase installed/waiting car un seul service worker peut être activé à la fois.

Le cycle de vie d'un service worker : analysé, installé/en attente, activé et redondant
Un cycle de vie simplifié d'un service worker ( Grand aperçu )

Ce n'est que lorsque toutes les fenêtres du navigateur contrôlées par le service worker précédent sont fermées que le nouveau service worker peut s'activer en toute sécurité.

Vous pouvez également contrôler cela manuellement en appelant skipWaiting() (ou self.skipWaiting() puisque self est le contexte d'exécution global dans le service worker). Cependant, la plupart du temps, vous ne devriez le faire qu'après avoir demandé à l'utilisateur s'il souhaite obtenir la dernière mise à jour.

Heureusement, workbox-window nous aide à y parvenir. Il s'agit d'une nouvelle bibliothèque de fenêtres introduite dans Workbox v4 qui vise à simplifier les tâches courantes du côté de la fenêtre.

Commençons par l'installer avec les éléments suivants :

 npm install workbox-window

Ensuite, importez Workbox en haut du fichier index.js :

 import { Workbox } from "workbox-window";

Ensuite, nous remplacerons notre code d'enregistrement par le code ci-dessous :

 if ("serviceWorker" in navigator) { window.addEventListener("load", () => { const wb = new Workbox("/sw.js"); wb.register(); }); }

On trouvera alors le bouton de mise à jour qui a pour ID mise à jour de l'application et écoutez l' workbox-waiting :

 //add before the wb.register() const updateButton = document.querySelector("#app-update"); // Fires when the registered service worker has installed but is waiting to activate. wb.addEventListener("waiting", event => { updateButton.classList.add("show"); updateButton.addEventListener("click", () => { // Set up a listener that will reload the page as soon as the previously waiting service worker has taken control. wb.addEventListener("controlling", event => { window.location.reload(); }); // Send a message telling the service worker to skip waiting. // This will trigger the `controlling` event handler above. wb.messageSW({ type: "SKIP_WAITING" }); }); });

Ce code affichera le bouton de mise à jour lorsqu'il y a une nouvelle mise à jour (donc lorsque l'agent de service est dans un état d'attente) et enverra un message SKIP_WAITING à l'agent de service.

Nous devrons mettre à jour le fichier de service worker et gérer l'événement SKIP_WAITING de sorte qu'il appelle le skipWaiting :

 //add in src-sw.js addEventListener("message", event => { if (event.data && event.data.type === "SKIP_WAITING") { skipWaiting(); });

Maintenant, lancez npm run dev puis rechargez la page. Allez dans votre code et mettez à jour le titre de la barre de navigation en "Navbar v2". Rechargez à nouveau la page et vous devriez pouvoir voir l'icône de mise à jour.

Emballer

Notre site Web fonctionne désormais hors ligne et est en mesure d'informer l'utilisateur des nouvelles mises à jour. Cependant, gardez à l'esprit que le facteur le plus important lors de la création d'une PWA est l'expérience utilisateur. Concentrez-vous toujours sur la création d'expériences faciles à utiliser par vos utilisateurs. En tant que développeurs, nous avons tendance à être trop enthousiasmés par la technologie et finissons souvent par oublier nos utilisateurs.

Si vous souhaitez aller plus loin, vous pouvez ajouter un manifeste d'application Web qui permettra à vos utilisateurs d'ajouter le site à leur écran d'accueil. Et si vous souhaitez en savoir plus sur Workbox, vous pouvez trouver la documentation officielle sur le site Web de Workbox.

Lectures complémentaires sur SmashingMag :

  • Pouvez-vous gagner plus d'argent avec une application mobile ou une PWA ?
  • Un guide complet des applications Web progressives
  • Native et PWA : des choix, pas des challengers !
  • Construire une PWA avec Angular 6