Créer une PWA chez Google (1re partie)

Ce que l'équipe Bulletin a appris sur les service workers lors du développement d'une PWA

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

Cet article est le premier d'une série d'articles de blog sur les enseignements tirés par l'équipe Google Bulletin lors de la création d'une PWA externe. Dans ces articles, nous partagerons certains des défis auxquels nous avons été confrontés, les approches que nous avons adoptées pour les surmonter et des conseils généraux pour éviter les écueils. Il ne s'agit en aucun cas d'une présentation complète des PWA. L'objectif est de partager les enseignements tirés de l'expérience de notre équipe.

Pour ce premier post, nous allons d'abord fournir quelques informations générales, puis nous allons nous plonger dans tout ce que nous avons appris sur les service workers.

Contexte

Bulletin a été en cours de développement de mi-2017 à mi-2019.

Pourquoi nous avons choisi de créer une PWA

Avant de nous plonger dans le processus de développement, examinons pourquoi la création d'une PWA était une option intéressante pour ce projet :

  • Capacité à effectuer des itérations rapidement : Particulièrement utile, car Bulletin sera testé sur plusieurs marchés.
  • Code base unique : Nos utilisateurs étaient répartis à peu près équitablement entre Android et iOS. Une PWA nous a permis de créer une seule application Web qui fonctionne sur les deux plates-formes. Cela a augmenté la vitesse et l'impact de l'équipe.
  • Mise à jour rapide et indépendante du comportement des utilisateurs Les PWA peuvent se mettre à jour automatiquement, ce qui réduit le nombre de clients obsolètes. Nous avons pu déployer des modifications destructives du backend en très peu de temps pour les clients.
  • Intégration facile aux applications propriétaires et tierces. Ce type d'intégration était obligatoire pour l'application. Avec une PWA, il suffisait souvent d'ouvrir une URL.
  • Il a supprimé les obstacles liés à l'installation d'une application.

Notre cadre

Pour Bulletin, nous avons utilisé Polymer, mais tout framework moderne et compatible fera l'affaire.

Ce que nous avons appris sur les service workers

Vous ne pouvez pas avoir de PWA sans worker de service. Les service workers offrent une grande puissance, par exemple des stratégies avancées de mise en cache, des fonctionnalités hors connexion, la synchronisation en arrière-plan, etc. Bien que les service workers ajoutent une certaine complexité, nous avons constaté que leurs avantages l'emportent sur cette complexité supplémentaire.

Générez-le si possible.

Évitez d'écrire un script de service worker manuellement. L'écriture manuelle des service workers nécessite de gérer manuellement les ressources mises en cache et la logique de réécriture commune à la plupart des bibliothèques de service workers, telles que Workbox.

Toutefois, en raison de notre pile technologique interne, nous n'avons pas pu utiliser de bibliothèque pour générer et gérer notre service worker. Nos enseignements ci-dessous en témoignent parfois. Pour en savoir plus, consultez la page Écueils des service workers non générés.

Toutes les bibliothèques ne sont pas compatibles avec les service workers

Certaines bibliothèques JavaScript font des suppositions qui ne fonctionnent pas comme prévu lorsqu'elles sont exécutées par un service worker. Par exemple, en supposant que window ou document soient disponibles, ou en utilisant une API non disponible pour les workers de service (XMLHttpRequest, stockage local, etc.). Assurez-vous que toutes les bibliothèques critiques dont vous avez besoin pour votre application sont compatibles avec les service workers. Pour cette PWA particulière, nous voulions utiliser gapi.js pour l'authentification, mais nous n'avons pas pu le faire, car il n'était pas compatible avec les service workers. Les auteurs de bibliothèques doivent également réduire ou supprimer les hypothèses inutiles sur le contexte JavaScript dans la mesure du possible pour prendre en charge les cas d'utilisation des services workers, par exemple en évitant les API incompatibles avec les services workers et en évitant l'état global.

Éviter d'accéder à IndexedDB lors de l'initialisation

Ne lisez pas IndexedDB lors de l'initialisation du script de service worker, sinon vous risquez de rencontrer ce problème:

  1. L'utilisateur dispose d'une application Web avec la version N d'IndexedDB (IDB)
  2. Nouvelle application Web transmise avec la version N+1 de l'IDB
  3. L'utilisateur accède à la PWA, ce qui déclenche le téléchargement du nouveau service worker.
  4. Le nouveau service worker lit à partir de l'IDB avant d'enregistrer le gestionnaire d'événements install, ce qui déclenche un cycle de mise à niveau de l'IDB pour passer de N à N+1.
  5. Étant donné que l'utilisateur dispose d'un ancien client avec la version N, le processus de mise à niveau du service worker se bloque, car les connexions actives sont toujours ouvertes à l'ancienne version de la base de données.
  6. Le service worker se bloque et ne s'installe jamais

Dans notre cas, le cache a été invalidé lors de l'installation d'un service worker. Par conséquent, si le service worker n'a jamais été installé, les utilisateurs ne reçoivent jamais l'application mise à jour.

Rendre votre application résiliente

Bien que les scripts de service worker s'exécutent en arrière-plan, ils peuvent également être arrêtés à tout moment, même au milieu d'opérations d'E/S (réseau, IDB, etc.). Tout processus de longue durée doit pouvoir être repris à tout moment.

Dans le cas d'un processus de synchronisation qui a importé de gros fichiers sur le serveur et les a enregistrés dans l'IDB, notre solution pour les importations partielles interrompues a consisté à exploiter le système de reprise de notre bibliothèque d'importation interne, en enregistrant l'URL d'importation avec reprise dans l'IDB avant l'importation, et en utilisant cette URL pour reprendre une importation si elle n'a pas été terminée la première fois. De plus, avant toute opération d'E/S de longue durée, l'état était enregistré dans l'IDB pour indiquer où nous en étions chaque enregistrement dans le processus.

Ne pas dépendre de l'état global

Étant donné que les services workers existent dans un contexte différent, de nombreux symboles que vous pourriez vous attendre à voir ne sont pas présents. Une grande partie de notre code s'exécutait à la fois dans un contexte window et dans un contexte de service worker (comme la journalisation, les indicateurs, la synchronisation, etc.). Le code doit être défensif concernant les services qu'il utilise, tels que le stockage local ou les cookies. Vous pouvez utiliser globalThis pour faire référence à l'objet global d'une manière qui fonctionne dans tous les contextes. Utilisez également les données stockées dans des variables globales avec parcimonie, car rien ne garantit que le script sera arrêté et que l'état sera évincé.

Développement local

L'un des composants majeurs des service workers consiste à mettre en cache les ressources localement. Toutefois, pendant le développement, c'est l'opposé de ce que vous souhaitez, en particulier lorsque les mises à jour sont effectuées de manière paresseuse. Vous devez toujours installer le nœud de calcul du serveur afin de pouvoir déboguer les problèmes le concernant ou utiliser d'autres API telles que la synchronisation en arrière-plan ou les notifications. Dans Chrome, vous pouvez y parvenir via les outils pour les développeurs Chrome en activant la case à cocher Ignorer pour le réseau (panneau Application > volet Service workers) et la case à cocher Désactiver le cache dans le panneau Réseau afin de désactiver également le cache de mémoire. Afin de couvrir plus de navigateurs, nous avons opté pour une solution différente en incluant un indicateur pour désactiver la mise en cache dans notre service worker, qui est activé par défaut dans les builds pour les développeurs. Les développeurs bénéficient ainsi toujours de leurs modifications les plus récentes, sans problème de mise en cache. Il est également important d'inclure l'en-tête Cache-Control: no-cache pour empêcher le navigateur de mettre en cache des composants.

Phare

Lighthouse fournit un certain nombre d'outils de débogage utiles pour les PWA. Il analyse un site et génère des rapports sur les PWA, les performances, l'accessibilité, le SEO et d'autres bonnes pratiques. Nous vous recommandons d'exécuter Lighthouse en intégration continue pour vous alerter si vous ne respectez pas l'un des critères d'une PWA. Cela nous est déjà arrivé une fois, où le service worker ne s'installait pas et que nous ne l'avons pas réalisé avant un transfert en production. L'intégration de Lighthouse dans notre CI aurait pu l'éviter.

Adopter la livraison continue

Étant donné que les services workers peuvent être mis à jour automatiquement, les utilisateurs ne peuvent pas limiter les mises à niveau. Cela réduit considérablement le nombre de clients obsolètes dans la nature. Lorsque l'utilisateur ouvrait notre application, le service worker a diffusé l'ancien client alors qu'il téléchargeait le nouveau client en différé. Une fois le nouveau client téléchargé, il invite l'utilisateur à actualiser la page pour accéder aux nouvelles fonctionnalités. Même si l'utilisateur a ignoré cette requête, la prochaine fois qu'il actualisera la page, il recevra la nouvelle version du client. Par conséquent, il est assez difficile pour un utilisateur de refuser les mises à jour de la même manière que pour les applications iOS/Android.

Nous avons pu déployer des modifications destructives du backend en un temps de migration très court pour les clients. En règle générale, nous accordons un mois aux utilisateurs pour qu'ils effectuent la mise à jour vers les nouveaux clients avant d'apporter des modifications destructives. Étant donné que l'application était diffusée alors qu'elle n'était pas actualisée, il était possible pour les clients plus anciens d'exister dans la nature si l'utilisateur ne l'avait pas ouverte depuis longtemps. Sur iOS, les service workers sont évincés après quelques semaines. Ce cas de figure ne se produit donc pas. Pour Android, ce problème peut être atténué en ne diffusant pas le contenu lorsqu'il est obsolète ou en l'exécutant manuellement au bout de quelques semaines. En pratique, nous n'avons jamais rencontré de problèmes liés à des clients obsolètes. Le niveau de sévérité d'une équipe donnée dépend de son cas d'utilisation spécifique, mais les PWA offrent une flexibilité nettement supérieure aux applications iOS/Android.

Obtenir les valeurs des cookies dans un service worker

Il est parfois nécessaire d'accéder aux valeurs des cookies dans un contexte de service worker. Dans notre cas, nous devions accéder aux valeurs des cookies pour générer un jeton permettant d'authentifier les requêtes d'API propriétaires. Dans un service worker, les API synchrones telles que document.cookies ne sont pas disponibles. Vous pouvez toujours envoyer un message aux clients actifs (fenêtres) à partir du service worker pour demander les valeurs des cookies, même si le service worker peut s'exécuter en arrière-plan sans client de fenêtre disponible, par exemple lors d'une synchronisation en arrière-plan. Pour contourner ce problème, nous avons créé un point de terminaison sur notre serveur frontend qui renvoyait simplement la valeur du cookie au client. Le service worker a envoyé une requête réseau à ce point de terminaison et a lu la réponse pour obtenir les valeurs des cookies.

Avec le lancement de l'API Cookie Store, cette solution de contournement ne devrait plus être nécessaire pour les navigateurs compatibles, car elle fournit un accès asynchrone aux cookies du navigateur et peut être utilisée directement par le service worker.

Écueils des service workers non générés

Assurez-vous que le script du service worker est modifié si un fichier statique mis en cache est modifié

Un modèle de PWA courant consiste à installer tous les fichiers d'application statiques pendant la phase install. Les clients peuvent ainsi accéder directement au cache de l'API Cache Storage pour toutes les visites suivantes. Les services workers ne sont installés que lorsque le navigateur détecte que le script du service worker a changé d'une manière ou d'une autre. Nous avons donc dû nous assurer que le fichier de script du service worker lui-même a changé d'une manière ou d'une autre lorsqu'un fichier mis en cache a changé. Nous l'avons fait manuellement en insérant un hachage du fichier de ressources statiques dans notre script de service worker. Ainsi, chaque version produisait un fichier JavaScript de service worker distinct. Les bibliothèques de service worker telles que Workbox automatisent ce processus pour vous.

Tests unitaires

Les API de service worker fonctionnent en ajoutant des écouteurs d'événements à l'objet global. Exemple :

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

Cela peut être pénible à tester, car vous devez simuler le déclencheur d'événement, l'objet événement, attendre le rappel respondWith(), puis attendre la promesse, avant d'effectuer une assertion sur le résultat. Un moyen plus simple de structurer cela consiste à déléguer toute l'implémentation vers un autre fichier, qui est plus facile à tester.

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

En raison des difficultés liées aux tests unitaires d'un script de service worker, nous avons conservé le script de service worker principal aussi basique que possible, en divisant la majeure partie de l'implémentation en d'autres modules. Étant donné que ces fichiers ne sont que des modules JS standards, ils peuvent être plus facilement testés avec des bibliothèques de test standards.

Ne manquez pas les parties 2 et 3 !

Dans les parties 2 et 3 de cette série, nous parlerons de la gestion des contenus multimédias et des problèmes spécifiques à iOS. Si vous souhaitez en savoir plus sur la création d'une PWA chez Google, consultez nos profils d'auteur pour savoir comment nous contacter :