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

Il s'agit du premier d'une série d'articles de blog sur les leçons tirées 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'un aperçu complet des PWA. L'objectif est de partager les enseignements tirés de l'expérience de notre équipe.

Dans ce premier post, nous allons commencer par aborder quelques informations de base, puis nous étudierons tout ce que nous avons appris sur les service workers.

Contexte

Bulletin a été activement développé entre mi-2017 et 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 permet 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 dans la nature. Nous avons pu déployer des modifications de rupture du backend avec un temps de migration très court 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 framework

Pour Bulletin, nous avons utilisé Polymer, mais n'importe quel framework moderne et bien pris en charge peut fonctionner.

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 services 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 afin de prendre en charge les cas d'utilisation de service worker, par exemple en évitant les API incompatibles avec un service worker et en évitant l'état global.

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

Ne lisez pas IndexedDB lors de l'initialisation de votre script de service worker, sinon vous risquez de vous retrouver dans cette situation indésirable:

  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. Un nouveau service worker lit 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 lors 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 importait des fichiers volumineux sur le serveur et les enregistrait dans l'IDB, notre solution pour les importations partielles interrompues consistait à exploiter le système avec 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 abouti 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 nous trouvions dans le processus pour chaque enregistrement.

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 de manière à ce qu'il fonctionne dans tous les contextes. Utilisez également les données stockées dans des variables globales avec parcimonie, car il n'est pas garanti que le script sera arrêté et que l'état sera supprimé.

Développement local

Un composant majeur des services workers consiste à mettre en cache des ressources en local. Cependant, lors du développement, il s'agit exactement de l'inverse de ce que vous souhaitez, en particulier lorsque les mises à jour sont effectuées de manière différée. 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

Comme les service workers peuvent se mettre à 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 servait l'ancien client tout en téléchargeant le nouveau client de manière paresseuse. Une fois le nouveau client téléchargé, l'utilisateur est invité à 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 de backend de rupture avec 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 travailleurs de service sont expulsés au bout de quelques semaines. Ce cas 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 avons dû accéder aux valeurs des cookies pour générer un jeton permettant d'authentifier les requêtes 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 la publication de l'API Cookie Store, ce correctif ne devrait plus être nécessaire pour les navigateurs compatibles, car il fournit un accès asynchrone aux cookies du navigateur et peut être utilisé directement par le service worker.

Pièges pour les 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 difficile à tester, car vous devez simuler le déclencheur d'événement, l'objet d'événement, attendre le rappel respondWith(), puis attendre la promesse, avant de finalement 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 n'étaient que des modules JS standards, ils pouvaient être plus facilement testés de manière unitaire à l'aide de 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 :