Un guide complet des applications Web progressives

Publié: 2022-03-10
Résumé rapide ↬ Dans cet article, nous examinerons les points faibles des utilisateurs qui naviguent sur d'anciens sites Web non PWA et la promesse des PWA de rendre le Web génial. Vous apprendrez la plupart des technologies importantes qui font des PWA sympas, comme les service workers, les notifications push Web et IndexedDB.

C'était l'anniversaire de mon père et je voulais lui commander un gâteau au chocolat et une chemise. Je me suis dirigé vers Google pour rechercher des gâteaux au chocolat et j'ai cliqué sur le premier lien dans les résultats de recherche. Il y avait un écran vide pendant quelques secondes; Je ne comprenais pas ce qui se passait. Après quelques secondes à regarder patiemment, mon écran mobile s'est rempli de délicieux gâteaux. Dès que j'ai cliqué sur l'un d'eux pour vérifier ses détails, j'ai reçu une grosse popup moche, me demandant d'installer une application Android afin que je puisse avoir une expérience soyeuse tout en commandant un gâteau.

C'était décevant. Ma conscience ne m'a pas permis de cliquer sur le bouton « Installer ». Tout ce que je voulais faire, c'était commander un petit gâteau et partir.

J'ai cliqué sur l'icône en forme de croix tout à droite de la fenêtre contextuelle pour en sortir dès que possible. Mais ensuite, la fenêtre contextuelle d'installation s'est installée au bas de l'écran, occupant un quart de l'espace. Et avec l'interface utilisateur feuilletée, le défilement vers le bas était un défi. J'ai en quelque sorte réussi à commander un gâteau hollandais.

Après cette terrible expérience, mon prochain défi était de commander une chemise pour mon père. Comme avant, je recherche sur Google des chemises. J'ai cliqué sur le premier lien, et en un clin d'œil, tout le contenu était juste devant moi. Le défilement était fluide. Pas de bannière d'installation. J'avais l'impression de naviguer sur une application native. Il y a eu un moment où ma terrible connexion Internet a abandonné, mais je pouvais toujours voir le contenu au lieu d'un jeu de dinosaures. Même avec mon Internet janky, j'ai réussi à commander une chemise et un jean pour mon père. Le plus surprenant de tous, je recevais des notifications concernant ma commande.

J'appellerais cela une expérience douce et soyeuse. Ces gens faisaient quelque chose de bien. Chaque site Web devrait le faire pour ses utilisateurs. C'est ce qu'on appelle une application Web progressive.

Comme le déclare Alex Russell dans l'un de ses articles de blog :

« Il arrive de temps en temps sur le Web que des technologies puissantes voient le jour sans l'aide de départements marketing ou d'emballages astucieux. Ils s'attardent et grandissent aux périphéries, devenant vieux chapeau pour un petit groupe tout en restant presque invisibles pour tout le monde. Jusqu'à ce que quelqu'un les nomme.

Une expérience fluide et soyeuse sur le Web, parfois connue sous le nom d'application Web progressive

Les applications Web progressives (PWA) sont davantage une méthodologie qui implique une combinaison de technologies pour créer des applications Web puissantes. Avec une expérience utilisateur améliorée, les gens passeront plus de temps sur les sites Web et verront plus de publicités. Ils ont tendance à acheter plus, et avec les mises à jour des notifications, ils sont plus susceptibles de visiter souvent. Le Financial Times a abandonné ses applications natives en 2011 et a créé une application Web utilisant les meilleures technologies disponibles à l'époque. Maintenant, le produit est devenu un PWA à part entière.

Mais pourquoi, après tout ce temps, créeriez-vous une application Web alors qu'une application native fait assez bien le travail ?

Examinons certaines des mesures partagées dans Google IO 17.

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

Cinq milliards d'appareils sont connectés au Web, faisant du Web la plus grande plate-forme de l'histoire de l'informatique. Sur le Web mobile, 11,4 millions de visiteurs uniques mensuels accèdent aux 1 000 meilleures propriétés Web et 4 millions aux 1 000 meilleures applications. Le Web mobile rassemble environ quatre fois plus d'utilisateurs que les applications natives. Mais ce nombre chute fortement lorsqu'il s'agit d'engagement.

Un utilisateur passe en moyenne 188,6 minutes dans les applications natives et seulement 9,3 minutes sur le Web mobile. Les applications natives tirent parti de la puissance des systèmes d'exploitation pour envoyer des notifications push afin de fournir aux utilisateurs des mises à jour importantes. Ils offrent une meilleure expérience utilisateur et démarrent plus rapidement que les sites Web dans un navigateur. Au lieu de saisir une URL dans le navigateur Web, les utilisateurs n'ont qu'à appuyer sur l'icône d'une application sur l'écran d'accueil.

Il est peu probable que la plupart des visiteurs sur le Web reviennent, alors les développeurs ont trouvé la solution de contournement en leur montrant des bannières pour installer des applications natives, dans le but de les maintenir profondément engagés. Mais alors, les utilisateurs devraient passer par la fastidieuse procédure d'installation du binaire d'une application native. Forcer les utilisateurs à installer une application est ennuyeux et réduit davantage les chances qu'ils l'installent en premier lieu. L'opportunité pour le web est claire.

Lecture recommandée : Native et PWA : des choix, pas des challengers !

Si les applications Web offrent une expérience utilisateur riche, des notifications push, une assistance hors ligne et un chargement instantané, elles peuvent conquérir le monde. C'est ce que fait une application Web progressive.

Une PWA offre une expérience utilisateur riche car elle possède plusieurs atouts :

  • Vite
    L'interface utilisateur n'est pas feuilletée. Le défilement est fluide. Et l'application répond rapidement à l'interaction de l'utilisateur.

  • Fiable
    Un site Web normal oblige les utilisateurs à attendre, sans rien faire, alors qu'il est occupé à faire des trajets vers le serveur. Une PWA, quant à elle, charge instantanément les données depuis le cache. Un PWA fonctionne de manière transparente, même sur une connexion 2G. Chaque requête réseau pour récupérer un actif ou un élément de données passe par un agent de service (plus sur cela plus tard), qui vérifie d'abord si la réponse à une requête particulière est déjà dans le cache. Lorsque les utilisateurs obtiennent du contenu réel presque instantanément, même avec une mauvaise connexion, ils font davantage confiance à l'application et la considèrent comme plus fiable.

  • Engageant
    Un PWA peut gagner une place sur l'écran d'accueil de l'utilisateur. Il offre une expérience de type application native en fournissant une zone de travail en plein écran. Il utilise des notifications push pour garder les utilisateurs engagés.

Maintenant que nous savons ce que les PWA apportent à la table, entrons dans les détails de ce qui donne aux PWA un avantage sur les applications natives. Les PWA sont construites avec des technologies telles que les service workers, les manifestes d'applications Web, les notifications push et la structure de données IndexedDB/locale pour la mise en cache. Examinons chacun en détail.

Travailleurs des services

Un service worker est un fichier JavaScript qui s'exécute en arrière-plan sans interférer avec les interactions de l'utilisateur. Toutes les requêtes GET au serveur passent par un service worker. Il agit comme un proxy côté client. En interceptant les requêtes réseau, il prend le contrôle total de la réponse renvoyée au client. Une PWA se charge instantanément car les service workers éliminent la dépendance au réseau en répondant avec les données du cache.

Un agent de service ne peut intercepter qu'une requête réseau qui se trouve dans sa portée. Par exemple, un agent de service de portée racine peut intercepter toutes les demandes de récupération provenant d'une page Web. Un travailleur de service fonctionne comme un système piloté par les événements. Il entre dans un état dormant lorsqu'il n'est pas nécessaire, préservant ainsi la mémoire. Pour utiliser un service worker dans une application web, nous devons d'abord l'enregistrer sur la page avec JavaScript.

 (function main () { /* navigator is a WEB API that allows scripts to register themselves and carry out their activities. */ if ('serviceWorker' in navigator) { console.log('Service Worker is supported in your browser') /* register method takes in the path of service worker file and returns a promises, which returns the registration object */ navigator.serviceWorker.register('./service-worker.js').then (registration => { console.log('Service Worker is registered!') }) } else { console.log('Service Worker is not supported in your browser') } })()

Nous vérifions d'abord si le navigateur prend en charge les service workers. Pour enregistrer un service worker dans une application web, nous fournissons son URL en paramètre à la fonction register , disponible dans navigator.serviceWorker ( navigator est une API web qui permet aux scripts de s'enregistrer et de mener à bien leurs activités). Un travailleur de service n'est enregistré qu'une seule fois. L'inscription ne se produit pas à chaque chargement de page. Le navigateur télécharge le fichier du service worker ( ./service-worker.js ) uniquement s'il existe une différence d'octet entre le service worker activé existant et le plus récent ou si son URL a changé.

Le service worker ci-dessus interceptera toutes les requêtes provenant de la racine ( / ). Pour limiter la portée d'un agent de service, nous passerions un paramètre facultatif avec l'une des clés comme portée.

 if ('serviceWorker' in navigator) { /* register method takes in an optional second parameter as an object. To restrict the scope of a service worker, the scope should be provided. scope: '/books' will intercept requests with '/books' in the url. */ navigator.serviceWorker.register('./service-worker.js', { scope: '/books' }).then(registration => { console.log('Service Worker for scope /books is registered', registration) }) }

Le technicien de service ci-dessus interceptera les requêtes contenant /books dans l'URL. Par exemple, il n'interceptera pas les requêtes avec /products , mais il pourrait très bien intercepter les requêtes avec /books/products .

Comme mentionné, un agent de service fonctionne comme un système piloté par les événements. Il écoute les événements (install, activate, fetch, push) et appelle en conséquence le gestionnaire d'événements respectif. Certains de ces événements font partie du cycle de vie d'un travailleur de service, qui passe par ces événements dans l'ordre pour être activé.

Installation

Une fois qu'un agent de service a été enregistré avec succès, un événement d'installation est déclenché. C'est un bon endroit pour effectuer le travail d'initialisation, comme la configuration du cache ou la création de magasins d'objets dans IndexedDB. (IndexedDB aura plus de sens pour vous une fois que nous entrerons dans ses détails. Pour l'instant, nous pouvons simplement dire qu'il s'agit d'une structure de paire clé-valeur.)

 self.addEventListener('install', (event) => { let CACHE_NAME = 'xyz-cache' let urlsToCache = [ '/', '/styles/main.css', '/scripts/bundle.js' ] event.waitUntil( /* open method available on caches, takes in the name of cache as the first parameter. It returns a promise that resolves to the instance of cache All the URLS above can be added to cache using the addAll method. */ caches.open(CACHE_NAME) .then (cache => cache.addAll(urlsToCache)) ) })

Ici, nous mettons en cache certains des fichiers afin que le prochain chargement soit instantané. self fait référence à l'instance de service worker. event.waitUntil fait attendre le service worker jusqu'à ce que tout le code qu'il contient ait fini de s'exécuter.

Activation

Une fois qu'un agent de service a été installé, il ne peut pas encore écouter les requêtes de récupération. Au lieu de cela, un événement activate est déclenché. Si aucun agent de service actif n'opère sur le site Web dans la même étendue, l'agent de service installé est immédiatement activé. Cependant, si un site Web a déjà un service worker actif, l'activation d'un nouveau service worker est retardée jusqu'à ce que tous les onglets fonctionnant sur l'ancien service worker soient fermés. Cela a du sens car l'ancien service worker utilise peut-être l'instance du cache qui est maintenant modifiée dans le plus récent. Ainsi, l'étape d'activation est un bon endroit pour se débarrasser des anciens caches.

 self.addEventListener('activate', (event) => { let cacheWhitelist = ['products-v2'] // products-v2 is the name of the new cache event.waitUntil( caches.keys().then (cacheNames => { return Promise.all( cacheNames.map( cacheName => { /* Deleting all the caches except the ones that are in cacheWhitelist array */ if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName) } }) ) }) ) })

Dans le code ci-dessus, nous supprimons l'ancien cache. Si le nom d'un cache ne correspond pas à la cacheWhitelist , il est alors supprimé. Pour ignorer la phase d'attente et activer immédiatement le service worker, nous utilisons skip.waiting() .

 self.addEventListener('activate', (event) => { self.skipWaiting() // The usual stuff })

Une fois le service worker activé, il peut écouter les requêtes de récupération et les événements push.

Récupérer le gestionnaire d'événements

Chaque fois qu'une page Web déclenche une demande de récupération d'une ressource sur le réseau, l'événement de récupération du service worker est appelé. Le gestionnaire d'événements d'extraction recherche d'abord la ressource demandée dans le cache. S'il est présent dans le cache, il renvoie la réponse avec la ressource mise en cache. Sinon, il lance une requête de récupération au serveur, et lorsque le serveur renvoie la réponse avec la ressource demandée, il la place dans le cache pour les requêtes suivantes.

 /* Fetch event handler for responding to GET requests with the cached assets */ self.addEventListener('fetch', (event) => { event.respondWith( caches.open('products-v2') .then (cache => { /* Checking if the request is already present in the cache. If it is present, sending it directly to the client */ return cache.match(event.request).then (response => { if (response) { console.log('Cache hit! Fetching response from cache', event.request.url) return response } /* If the request is not present in the cache, we fetch it from the server and then put it in cache for subsequent requests. */ fetch(event.request).then (response => { cache.put(event.request, response.clone()) return response }) }) }) ) })

event.respondWith permet au service worker d'envoyer une réponse personnalisée au client.

Offline-first est maintenant une chose. Pour toute demande non critique, nous devons servir la réponse depuis le cache, au lieu de faire un tour vers le serveur. Si un actif n'est pas présent dans le cache, nous l'obtenons du serveur, puis le mettons en cache pour les demandes ultérieures.

Les techniciens de service ne travaillent que sur des sites Web HTTPS car ils ont le pouvoir de manipuler la réponse de toute demande de récupération. Quelqu'un avec une intention malveillante pourrait falsifier la réponse à une requête sur un site Web HTTP. Ainsi, l'hébergement d'une PWA sur HTTPS est obligatoire. Les travailleurs de service n'interrompent pas le fonctionnement normal du DOM. Ils ne peuvent pas communiquer directement avec la page Web. Pour envoyer un message à une page Web, il utilise des messages postaux.

Notifications poussées Web

Supposons que vous soyez occupé à jouer à un jeu sur votre mobile et qu'une notification s'affiche vous informant d'une réduction de 30 % sur votre marque préférée. Sans plus tarder, vous cliquez sur la notification et magasinez votre souffle. Obtenir des mises à jour en direct sur, par exemple, un match de cricket ou de football ou recevoir des e-mails et des rappels importants sous forme de notifications est un gros problème lorsqu'il s'agit d'engager les utilisateurs avec un produit. Cette fonctionnalité n'était disponible que dans les applications natives jusqu'à l'arrivée de PWA. Une PWA utilise des notifications push Web pour concurrencer cette fonctionnalité puissante que les applications natives fournissent prêtes à l'emploi. Un utilisateur recevrait toujours une notification push Web même si la PWA n'est ouverte dans aucun des onglets du navigateur et même si le navigateur n'est pas ouvert.

Une application Web doit demander l'autorisation de l'utilisateur pour lui envoyer des notifications push.

Invite du navigateur pour demander l'autorisation pour les notifications Web Push
Invite du navigateur pour demander l'autorisation pour les notifications Web Push. ( Grand aperçu )

Une fois que l'utilisateur a confirmé en cliquant sur le bouton "Autoriser", un jeton d'abonnement unique est généré par le navigateur. Ce jeton est unique pour cet appareil. Le format du jeton d'abonnement généré par Chrome est le suivant :

 { "endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS", "expirationTime": null, "keys": { "p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w", "auth": "TJXqKozSJxcWvtQasEUZpQ" } }

Le point de endpoint contenu dans le jeton ci-dessus sera unique pour chaque abonnement. Sur un site Web moyen, des milliers d'utilisateurs accepteraient de recevoir des notifications push, et pour chacun d'eux, ce point de endpoint serait unique. Ainsi, à l'aide de ce point de endpoint , l'application est capable de cibler ces utilisateurs à l'avenir en leur envoyant des notifications push. expirationTime est la durée pendant laquelle l'abonnement est valide pour un appareil particulier. Si expirationTime est de 20 jours, cela signifie que l'abonnement push de l'utilisateur expirera après 20 jours et que l'utilisateur ne pourra pas recevoir de notifications push sur l'ancien abonnement. Dans ce cas, le navigateur générera un nouveau jeton d'abonnement pour cet appareil. Les clés auth et p256dh sont utilisées pour le chiffrement.

Maintenant, pour envoyer des notifications push à ces milliers d'utilisateurs à l'avenir, nous devons d'abord enregistrer leurs jetons d'abonnement respectifs. C'est le travail du serveur d'application (le serveur principal, peut-être un script Node.js) d'envoyer des notifications push à ces utilisateurs. Cela peut sembler aussi simple que d'envoyer une requête POST à ​​l'URL du point de terminaison avec les données de notification dans la charge utile de la requête. Cependant, il convient de noter que si un utilisateur n'est pas en ligne lorsqu'une notification push qui lui est destinée est déclenchée par le serveur, il devrait toujours recevoir cette notification une fois qu'il se reconnectera. Le serveur devrait prendre en charge de tels scénarios, ainsi que l'envoi de milliers de requêtes aux utilisateurs. Un serveur gardant une trace de la connexion de l'utilisateur semble compliqué. Ainsi, quelque chose au milieu serait responsable du routage des notifications push Web du serveur vers le client. C'est ce qu'on appelle un service push, et chaque navigateur a sa propre implémentation d'un service push. Le navigateur doit indiquer les informations suivantes au service push afin d'envoyer une notification :

  1. Le temps de vivre
    Il s'agit de la durée pendant laquelle un message doit être mis en file d'attente, au cas où il ne serait pas remis à l'utilisateur. Une fois ce temps écoulé, le message sera supprimé de la file d'attente.
  2. Urgence du message
    Ainsi, le service push préserve la batterie de l'utilisateur en n'envoyant que des messages de haute priorité.

Le service push achemine les messages vers le client. Étant donné que le push doit être reçu par le client même si son application Web respective n'est pas ouverte dans le navigateur, les événements push doivent être écoutés par quelque chose qui surveille en permanence en arrière-plan. Vous l'avez deviné : c'est le travail du travailleur de service. Le technicien de service écoute les événements push et affiche les notifications à l'utilisateur.

Nous savons donc maintenant que le navigateur, le service push, le service worker et le serveur d'applications fonctionnent en harmonie pour envoyer des notifications push à l'utilisateur. Examinons les détails de mise en œuvre.

Client Push Web

Demander la permission à l'utilisateur est une chose unique. Si un utilisateur a déjà accordé l'autorisation de recevoir des notifications push, nous ne devrions pas demander à nouveau. La valeur d'autorisation est enregistrée dans Notification.permission .

 /* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) } /* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }

Dans la méthode subscribe ci-dessus, nous transmettons userVisibleOnly et applicationServerKey pour générer un jeton d'abonnement. La propriété userVisibleOnly doit toujours être vraie car elle indique au navigateur que toute notification push envoyée par le serveur sera affichée au client. Pour comprendre le but de applicationServerKey , considérons un scénario.

Si une personne met la main sur vos milliers de jetons d'abonnement, elle pourrait très bien envoyer des notifications aux terminaux contenus dans ces abonnements. Il n'y a aucun moyen pour le point de terminaison d'être lié à votre identité unique. Pour fournir une identité unique aux jetons d'abonnement générés sur votre application Web, nous utilisons le protocole VAPID. Avec VAPID, le serveur d'application s'identifie volontairement auprès du service push lors de l'envoi de notifications push. Nous générons deux clés comme suit :

 const webpush = require('web-push') const vapidKeys = webpush.generateVAPIDKeys()

web-push est un module npm. vapidKeys aura une clé publique et une clé privée. La clé du serveur d'application utilisée ci-dessus est la clé publique.

Serveur Web Push

Le travail du serveur push Web (serveur d'application) est simple. Il envoie une charge utile de notification aux jetons d'abonnement.

 const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options) const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options) const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)

Il utilise la méthode sendNotification de la bibliothèque push Web.

Travailleurs des services

Le technicien de service affiche la notification à l'utilisateur comme suit :

 self.addEventListener('push', (event) => { let options = { body: event.data.body, icon: 'images/example.png', } event.waitUntil( /* The showNotification method is available on the registration object of the service worker. The first parameter to showNotification method is the title of notification, and the second parameter is an object */ self.registration.showNotification(event.data.title, options) ) })

Jusqu'à présent, nous avons vu comment un service worker utilise le cache pour stocker les requêtes et rendre une PWA rapide et fiable, et nous avons vu comment les notifications push Web maintiennent l'engagement des utilisateurs.

Pour stocker un tas de données côté client pour un support hors ligne, nous avons besoin d'une structure de données géante. Examinons la PWA du Financial Times. Vous devez être témoin de la puissance de cette structure de données par vous-même. Chargez l'URL dans votre navigateur, puis désactivez votre connexion Internet. Recharge la page. Gah ! Est-ce que ça marche encore ? Il est. (Comme je l'ai dit, hors ligne est le nouveau noir.) Les données ne proviennent pas des fils. Il est servi depuis la maison. Rendez-vous sur l'onglet "Applications" des outils de développement Chrome. Sous "Stockage", vous trouverez "IndexedDB".

IndexedDB stocke les données des articles dans Financial Times PWA
IndexedDB sur Financial Times PWA. ( Grand aperçu )

Découvrez la boutique d'objets "Articles" et développez l'un des éléments pour voir la magie par vous-même. Le Financial Times a stocké ces données pour une assistance hors ligne. Cette structure de données qui nous permet de stocker une quantité massive de données s'appelle IndexedDB. IndexedDB est une base de données orientée objet basée sur JavaScript pour stocker des données structurées. Nous pouvons créer différents magasins d'objets dans cette base de données à des fins diverses. Par exemple, comme nous pouvons le voir dans l'image ci-dessus, "Resources", "ArticleImages" et "Articles" sont appelés des magasins d'objets. Chaque enregistrement d'un magasin d'objets est identifié de manière unique par une clé. IndexedDB peut même être utilisé pour stocker des fichiers et des blobs.

Essayons de comprendre IndexedDB en créant une base de données pour stocker des livres.

 let openIdbRequest = window.indexedDB.open('booksdb', 1)

Si la base de données booksdb n'existe pas déjà, le code ci-dessus créera une base de données booksdb . Le deuxième paramètre de la méthode open est la version de la base de données. La spécification d'une version prend en charge les modifications liées au schéma qui pourraient se produire à l'avenir. Par exemple, booksdb n'a maintenant qu'une seule table, mais lorsque l'application grandit, nous avons l'intention d'y ajouter deux autres tables. Pour nous assurer que notre base de données est synchronisée avec le schéma mis à jour, nous spécifierons une version supérieure à la précédente.

L'appel de la méthode open n'ouvre pas la base de données immédiatement. C'est une requête asynchrone qui renvoie un objet IDBOpenDBRequest . Cet objet a des propriétés de succès et d'erreur ; nous devrons écrire des gestionnaires appropriés pour ces propriétés afin de gérer l'état de notre connexion.

 let dbInstance openIdbRequest.onsuccess = (event) => { dbInstance = event.target.result console.log('booksdb is opened successfully') } openIdbRequest.onerror = (event) => { console.log('There was an error in opening booksdb database') } openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result let objectstore = db.createObjectStore('books', { keyPath: 'id' }) }

Pour gérer la création ou la modification des magasins d'objets (les magasins d'objets sont analogues aux tables basées sur SQL — ils ont une structure clé-valeur), la méthode onupgradeneeded est appelée sur l'objet openIdbRequest . La méthode onupgradeneeded sera appelée chaque fois que la version changera. Dans l'extrait de code ci-dessus, nous créons un magasin d'objets de livres avec une clé unique comme ID.

Disons qu'après avoir déployé ce morceau de code, nous devons créer un autre magasin d'objets, appelé as users . Donc, maintenant la version de notre base de données sera 2 .

 let openIdbRequest = window.indexedDB.open('booksdb', 2) // New Version - 2 /* Success and error event handlers remain the same. The onupgradeneeded method gets called when the version of the database changes. */ openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result if (!db.objectStoreNames.contains('books')) { let objectstore = db.createObjectStore('books', { keyPath: 'id' }) } let oldVersion = event.oldVersion let newVersion = event.newVersion /* The users tables should be added for version 2. If the existing version is 1, it will be upgraded to 2, and the users object store will be created. */ if (oldVersion === 1) { db.createObjectStore('users', { keyPath: 'id' }) } }

Nous avons mis en cache dbInstance dans le gestionnaire d'événements de réussite de la requête ouverte. Pour récupérer ou ajouter des données dans IndexedDB, nous utiliserons dbInstance . Ajoutons quelques enregistrements de livres dans notre magasin d'objets de livres.

 let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') let bookRecord = { id: '1', name: 'The Alchemist', author: 'Paulo Coelho' } let addBookRequest = objectstore.add(bookRecord) addBookRequest.onsuccess = (event) => { console.log('Book record added successfully') } addBookRequest.onerror = (event) => { console.log('There was an error in adding book record') }

Nous utilisons des transactions , en particulier lors de l'écriture d'enregistrements sur des magasins d'objets. Une transaction est simplement un wrapper autour d'une opération pour assurer l'intégrité des données. Si l'une des actions d'une transaction échoue, aucune action n'est effectuée sur la base de données.

Modifions un enregistrement de livre avec la méthode put :

 let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter modifyBookRequest.onsuccess = (event) => { console.log('Book record updated successfully') }

Récupérons un enregistrement de livre avec la méthode get :

 let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') /* get method takes in the id of the record */ let getBookRequest = objectstore.get(1) getBookRequest.onsuccess = (event) => { /* event.target.result contains the matched record */ console.log('Book record', event.target.result) } getBookRequest.onerror = (event) => { console.log('Error while retrieving the book record.') }

Ajouter une icône sur l'écran d'accueil

Maintenant qu'il n'y a pratiquement plus de distinction entre une PWA et une application native, il est logique d'offrir une place de choix à la PWA. Si votre site Web remplit les critères de base d'un PWA (hébergé sur HTTPS, s'intègre aux service workers et possède un manifest.json ) et après que l'utilisateur a passé un certain temps sur la page Web, le navigateur appellera une invite en bas, demandant à l'utilisateur d'ajouter l'application à son écran d'accueil, comme indiqué ci-dessous :

Invite à ajouter Financial Times PWA sur l'écran d'accueil
Invite à ajouter Financial Times PWA sur l'écran d'accueil. ( Grand aperçu )

Lorsqu'un utilisateur clique sur "Ajouter FT à l'écran d'accueil", la PWA peut mettre le pied sur l'écran d'accueil, ainsi que dans le tiroir de l'application. Lorsqu'un utilisateur recherche une application sur son téléphone, tous les PWA correspondant à la requête de recherche sont répertoriés. Ils seront également visibles dans les paramètres système, ce qui permet aux utilisateurs de les gérer facilement. En ce sens, une PWA se comporte comme une application native.

Les PWA utilisent manifest.json pour fournir cette fonctionnalité. Examinons un simple fichier manifest.json .

 { "name": "Demo PWA", "short_name": "Demo", "start_url": "/?standalone", "background_color": "#9F0C3F", "theme_color": "#fff1e0", "display": "standalone", "icons": [{ "src": "/lib/img/icons/xxhdpi.png?v2", "sizes": "192x192" }] }

Le short_name apparaît sur l'écran d'accueil de l'utilisateur et dans les paramètres système. Le name apparaît dans l'invite de chrome et sur l'écran de démarrage. L'écran de démarrage est ce que l'utilisateur voit lorsque l'application se prépare à être lancée. Le start_url est l'écran principal de votre application. C'est ce que les utilisateurs obtiennent lorsqu'ils appuient sur une icône sur l'écran d'accueil. Le background_color est utilisé sur l'écran de démarrage. Le theme_color définit la couleur de la barre d'outils. La valeur standalone pour le mode d' display indique que l'application doit être utilisée en mode plein écran (en masquant la barre d'outils du navigateur). Lorsqu'un utilisateur installe une PWA, sa taille est simplement exprimée en kilo-octets, plutôt qu'en mégaoctets d'applications natives.

Les travailleurs de service, les notifications push Web, IndexedDB et la position de l'écran d'accueil compensent le support, la fiabilité et l'engagement hors ligne. Il convient de noter qu'un travailleur de service ne prend pas vie et ne commence pas à faire son travail dès le premier chargement. Le premier chargement sera toujours lent jusqu'à ce que tous les actifs statiques et autres ressources aient été mis en cache. On peut mettre en place quelques stratégies pour optimiser le premier chargement.

Regroupement d'actifs

Toutes les ressources, y compris le HTML, les feuilles de style, les images et le JavaScript, doivent être récupérées sur le serveur. Plus il y a de fichiers, plus il faut de requêtes HTTPS pour les récupérer. Nous pouvons utiliser des bundlers comme WebPack pour regrouper nos actifs statiques, réduisant ainsi le nombre de requêtes HTTP au serveur. WebPack fait un excellent travail pour optimiser davantage le bundle en utilisant des techniques telles que le fractionnement de code (c'est-à-dire en ne regroupant que les fichiers nécessaires au chargement de la page en cours, au lieu de les regrouper tous ensemble) et en secouant l'arborescence (c'est-à-dire en supprimant les dépendances en double ou dépendances importées mais non utilisées dans le code).

Réduire les allers-retours

L'une des principales raisons de la lenteur du Web est la latence du réseau. Le temps nécessaire à un octet pour passer de A à B varie en fonction de la connexion réseau. Par exemple, un aller-retour particulier via Wi-Fi prend 50 millisecondes et 500 millisecondes sur une connexion 3G, mais 2500 millisecondes sur une connexion 2G. Ces requêtes sont envoyées à l'aide du protocole HTTP, ce qui signifie que lorsqu'une connexion particulière est utilisée pour une requête, elle ne peut pas être utilisée pour d'autres requêtes tant que la réponse de la requête précédente n'est pas servie. Un site Web peut effectuer six requêtes HTTP asynchrones à la fois, car six connexions sont disponibles sur un site Web pour effectuer des requêtes HTTP. Un site Web moyen effectue environ 100 requêtes ; ainsi, avec un maximum de six connexions disponibles, un utilisateur peut finir par passer environ 833 millisecondes en un seul aller-retour. (Le calcul est de 833 millisecondes - 1006 = 1666 . Nous devons diviser 1666 par 2 car nous calculons le temps passé sur un aller-retour.) Avec HTTP2 en place, le délai d'exécution est considérablement réduit. HTTP2 ne bloque pas la tête de connexion, de sorte que plusieurs requêtes peuvent être envoyées simultanément.

La plupart des réponses HTTP contiennent des en-têtes last-modified et etag . L'en last-modified correspond à la date à laquelle le fichier a été modifié pour la dernière fois, et un etag est une valeur unique basée sur le contenu du fichier. Il ne sera modifié que lorsque le contenu d'un fichier est modifié. Ces deux en-têtes peuvent être utilisés pour éviter de télécharger à nouveau le fichier si une version en cache est déjà disponible localement. Si le navigateur dispose d'une version de ce fichier disponible localement, il peut ajouter l'un de ces deux en-têtes dans la requête en tant que tel :

Ajoutez des ETag et des en-têtes de dernière modification pour empêcher le téléchargement d'actifs en cache valides
ETag et en-têtes de dernière modification. ( Grand aperçu )

Le serveur peut vérifier si le contenu du fichier a changé. If the contents of the file have not changed, then it responds with a status code of 304 ( not modified ).

If-None-Match Header to prevent downloading of valid cached assets
If-None-Match Header. ( Grand aperçu )

This indicates to the browser to use the locally available cached version of the file. By doing all of this, we've prevented the file from being downloaded.

Faster responses are in now place, but our job is not done yet. We still have to parse the HTML, load the style sheets and make the web page interactive. It makes sense to show some empty boxes with a loader to the user, instead of a blank screen. While the HTML document is getting parsed, when it comes across <script src='asset.js'></script> , it will make a synchronous HTTP request to the server to fetch asset.js , and the whole parsing process will be paused until the response comes back. Imagine having a dozen of synchronous static asset references. These could very well be managed just by making use of the async keyword in script references, like <script src='asset.js' async></script> . With the introduction of the async keyword here, the browser will make an asynchronous request to fetch asset.js without hindering the parsing of the HTML. If a script file is required at a later stage, we can defer the downloading of that file until the entire HTML has been parsed. A script file can be deferred by using the defer keyword, like <script src='asset.js' defer></script> .

Conclusion

We've learned a lot of many new things that make for a cool web application. Here's a summary of all of the things we've explored in this article:

  1. Service workers make good use of the cache to speed up the loading of assets.
  2. Web push notifications work under the hood.
  3. We use IndexedDB to store a massive amount of data.
  4. Some of the optimizations for instant first load, like using HTTP2 and adding headers like Etag , last-modified and If-None-Match , prevent the downloading of valid cached assets.

That's all, folks!