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

Ce document 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 présentons 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 pièges. Il ne s'agit en aucun cas d'un aperçu complet des PWA. L'objectif est de partager les leçons tirées de l'expérience de notre équipe.

Pour ce premier article, nous aborderons d'abord quelques informations générales, puis nous examinerons tout ce que nous avons appris sur les service workers.

Contexte

Bulletin a été activement développé de mi-2017 à mi-2019.

Pourquoi nous avons choisi de créer une PWA

Avant d'examiner le processus de développement, examinons en quoi la création d'une PWA était une option intéressante pour ce projet:

  • Possibilité d'effectuer des itérations rapidement. C'est particulièrement utile, car Bulletin serait testé sur plusieurs marchés.
  • Code base unique : Nos utilisateurs étaient à peu près équitablement répartis entre Android et iOS. Une PWA nous permettait de créer une seule application Web qui fonctionnerait sur les deux plates-formes. Cela a augmenté la vitesse et l'impact de l'équipe.
  • Mises à jour rapides et indépendantes 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 dans un délai de migration très court pour les clients.
  • Facilité d'intégration à des applications propriétaires et tierces Ces intégrations étaient indispensables pour l'application. Avec une PWA, cela signifiait souvent simplement ouvrir une URL.
  • Élimination des difficultés liées à l'installation d'une application

Notre cadre

Pour Bulletin, nous avons utilisé Polymer, mais n'importe quel framework moderne bien compatible fonctionne.

Ce que nous avons appris sur les service workers

Vous ne pouvez pas créer de PWA sans service worker. Les service workers vous offrent une grande puissance, par exemple des stratégies avancées de mise en cache, des fonctionnalités hors connexion, une 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 vous le pouvez

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

Cela dit, en raison de notre pile technologique interne, nous ne pouvions pas utiliser de bibliothèque pour générer et gérer notre service worker. Les leçons tirées ci-dessous le refléteront parfois. Pour en savoir plus, consultez la section Pièges concernant les service workers non générés.

Toutes les bibliothèques ne sont pas compatibles avec un nœud de calcul de service

Certaines bibliothèques JavaScript émettent des hypothèses 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 sont disponibles, ou si vous utilisez une API non disponible pour les service workers (XMLHttpRequest, stockage local, etc.). Assurez-vous que toutes les bibliothèques essentielles dont vous avez besoin pour votre application sont compatibles avec les nœuds de calcul du service. Pour cette PWA, nous voulions utiliser gapi.js pour l'authentification, mais nous n'avons pas pu le faire, car elle n'était pas compatible avec les service workers. Dans la mesure du possible, les auteurs de bibliothèques doivent également réduire ou supprimer les hypothèses inutiles sur le contexte JavaScript afin de prendre en charge les cas d'utilisation des service workers, par exemple en évitant les API incompatibles avec les nœuds de calcul du service et en évitant l'état global.

Éviter d'accéder à IndexedDB pendant l'initialisation

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

  1. L'utilisateur dispose d'une application Web avec IndexedDB (IDB) version N
  2. La nouvelle application Web est déployée avec la version N+1 de l'IDB
  3. L'utilisateur accède à une PWA, ce qui déclenche le téléchargement d'un nouveau service worker
  4. Un 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. Comme l'utilisateur possède 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 n'était plus valide lors de l'installation du service worker. Par conséquent, si le service worker n'a jamais été installé, les utilisateurs n'ont jamais reçu l'application mise à jour.

La rendre 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 être réactivable à tout moment.

Dans le cas d'un processus de synchronisation qui importait des fichiers volumineux sur le serveur et enregistrés sur 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 IDB avant l'importation et en utilisant cette URL pour reprendre une importation si celle-ci n'avait pas abouti la première fois. De même, avant toute opération d'E/S de longue durée, l'état était enregistré dans IDB pour indiquer où nous étions dans le processus pour chaque enregistrement.

Ne pas dépendre d'un état global

Étant donné que les service workers existent dans un contexte différent, de nombreux symboles auxquels vous pourriez vous attendre ne sont pas présents. Une grande partie de notre code s'exécute à la fois dans un contexte window et dans un contexte de service worker (par exemple, journalisation, options, synchronisation, etc.). Le code doit se prémunir contre 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 fonctionnera dans tous les contextes. Utilisez également les données stockées dans les variables globales avec parcimonie, car il n'y a aucune garantie quant au moment où le script sera arrêté et l'état évincé.

Développement local

La mise en cache des ressources en local est un composant majeur des service workers. Toutefois, pendant le développement, il s'agit exactement de l'opposé de ce que vous souhaitez, en particulier lorsque les mises à jour sont effectuées en différé. Vous souhaitez toujours que le nœud de calcul du serveur soit installé pour pouvoir déboguer les problèmes s'y rapportant ou travailler avec d'autres API telles que la synchronisation en arrière-plan ou les notifications. Dans Chrome, les outils pour les développeurs Chrome permettent d'effectuer cette opération en cochant la case Contourner pour le réseau (panneau Application > volet Nœuds de calcul) en plus de la case Désactiver le cache dans le panneau Réseau. Pour couvrir davantage de navigateurs, nous avons choisi une autre solution en incluant un indicateur permettant de désactiver la mise en cache dans notre service worker, qui est activé par défaut sur les builds de développement. Ainsi, les développeurs obtiennent toujours les modifications les plus récentes sans aucun problème de mise en cache. Il est important d'inclure également l'en-tête Cache-Control: no-cache pour empêcher le navigateur de mettre en cache des éléments.

Phare

Lighthouse fournit un certain nombre d'outils de débogage utiles pour les PWA. Elle 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 sur l'intégration continue afin de vous avertir si vous ne respectez pas l'un des critères pour devenir une PWA. Cela nous est arrivé une fois, lorsque le service worker n'installait pas le service et que nous ne l'avions pas réalisé avant le déploiement en production. L'intégration de Lighthouse à notre CI aurait permis d'éviter cela.

Adopter la livraison continue

Étant donné que les service workers peuvent effectuer les mises à jour automatiquement, les utilisateurs ne peuvent pas les limiter. Cela réduit considérablement le nombre de clients obsolètes dans le monde réel. Lorsque l'utilisateur ouvrait notre application, le service worker affichait l'ancien client alors qu'il téléchargeait le nouveau client en différé. 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 actualise la page, il recevra la nouvelle version du client. Par conséquent, il est assez difficile pour un utilisateur de refuser les mises à jour comme 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 aux utilisateurs un mois pour passer à des clients plus récents avant d'effectuer des modifications importantes. Étant donné que l'application était diffusée tant qu'elle n'était pas actualisée, il était en fait possible que les clients plus anciens existent dans la nature si l'utilisateur ne l'avait pas ouverte depuis longtemps. Sur iOS, les service workers sont évincés après deux 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 s'il est obsolète ou en faisant expirer manuellement le contenu après quelques semaines. En pratique, nous n’avons jamais rencontré de problèmes de clients obsolètes. Le degré de rigueur d'une équipe donnée dépend de son cas d'utilisation spécifique. Toutefois, les PWA offrent beaucoup plus de flexibilité que les applications iOS/Android.

Obtenir des valeurs de cookie auprès d'un service worker

Il est parfois nécessaire d'accéder aux valeurs des cookies dans le contexte d'un 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 du service worker aux clients actifs (fenêtrés) pour demander les valeurs des cookies. Toutefois, il peut s'exécuter en arrière-plan sans qu'aucun client fenêtré n'est 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 renvoie simplement la valeur du cookie au client. Le service worker a envoyé une requête réseau à ce point de terminaison et lit la réponse pour obtenir les valeurs des cookies.

Avec le lancement de l'API Cookie Store, cette solution 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.

Problèmes pour les service workers non générés

Assurez-vous que le script du service worker change en cas de modification d'un fichier statique mis en cache.

Un modèle de PWA courant permet à un service worker d'installer tous les fichiers d'application statiques pendant sa phase install, ce qui permet aux clients d'appeler directement le cache de l'API Cache Storage pour toutes les visites ultérieures . Les service workers ne sont installés que lorsque le navigateur détecte que leur script 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 était modifié d'une manière ou d'une autre lorsqu'un fichier mis en cache a changé. Nous l'avons fait manuellement en intégrant un hachage de l'ensemble de fichiers de ressources statiques dans notre script de service worker. Chaque version génère donc un fichier JavaScript de service worker distinct. Les bibliothèques de service workers 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 s'avérer pénible à tester, car vous devez simuler le déclencheur d'événements, l'objet d'événement, attendre le rappel respondWith(), puis attendre la promesse avant de valider le résultat. Un moyen plus simple de structurer cela consiste à déléguer toute l'implémentation à un autre fichier, ce 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 gardé le script principal du service worker aussi simple que possible, en répartissant la majeure partie de l'implémentation dans d'autres modules. Étant donné que ces fichiers n'étaient que des modules JavaScript standards, ils pourraient être plus facilement soumis à des tests unitaires avec des bibliothèques de test standards.

Restez à l'écoute pour les parties 2 et 3

Dans les deux et trois parties de cette série, nous aborderons la gestion multimédia et les problèmes spécifiques à iOS. Si vous souhaitez en savoir plus sur la création d'une PWA chez Google, consultez nos profils d'auteurs pour savoir comment nous contacter: