Ce que l'équipe Bulletin a appris sur les service workers lors du développement d'une PWA
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'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.
- Elles sont mises à jour rapidement et indépendamment 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. Ces intégrations étaient obligatoires pour l'application. Avec une PWA, cela signifiait souvent simplement 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 vous offrent de nombreuses fonctionnalités, telles que des stratégies de mise en cache avancées, des fonctionnalités hors connexion, une synchronisation en arrière-plan, etc. Bien que les service workers ajoutent de la complexité, nous avons constaté que leurs avantages l'emportaient sur cette complexité.
Générez-le si possible.
Évitez d'écrire un script de service worker manuellement. Écrire des services workers manuellement nécessite de gérer manuellement les ressources mises en cache et de réécrire la logique commune à la plupart des bibliothèques de services 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 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 de votre script de service worker, sinon vous risquez de vous retrouver dans cette situation indésirable:
- L'utilisateur dispose d'une application Web avec la version N d'IndexedDB (IDB)
- Nouvelle application Web transmise avec la version N+1 de l'IDB
- L'utilisateur accède à la PWA, ce qui déclenche le téléchargement du nouveau service worker.
- 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. - É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.
- Le service worker se bloque et ne s'installe jamais
Dans notre cas, le cache a été invalidé lors de l'installation du service worker. Par conséquent, si le service worker ne s'est jamais installé, les utilisateurs n'ont jamais reçu 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 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. 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 souhaitez toujours que le worker de serveur soit installé afin de pouvoir déboguer les problèmes ou travailler avec 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 du développeur. Cela garantit que les développeurs obtiennent toujours 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 plusieurs 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 service 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 servait l'ancien client tout en téléchargeant le nouveau client de manière paresseuse. 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 de rupture du backend avec un temps de migration très court pour les clients. En règle générale, nous donnons aux utilisateurs un mois pour passer à des clients plus récents avant d'apporter des modifications importantes. Étant donné que l'application était diffusée alors qu'elle était obsolète, il était possible que des clients plus anciens existent dans la nature si l'utilisateur n'avait pas ouvert l'application 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 le faisant expirer 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 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.
É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 PWA courant consiste à installer tous les fichiers d'application statiques par un service worker lors de sa phase install
, ce qui permet aux clients d'accéder directement au cache de l'API Cache Storage pour toutes les visites ultérieures . 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. Une façon plus simple de structurer cela consiste à déléguer toute l'implémentation à 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: