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 posts, 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 enseignements tirés de l'expérience de notre équipe.

Dans ce premier post, nous allons d'abord 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 d'entrer dans le processus de développement, voyons pourquoi la création d'une PWA était une option intéressante pour ce projet:

  • Capacité à effectuer des itérations rapidement : Cette approche est particulièrement utile, car Bulletin serait testé sur plusieurs marchés.
  • Code base unique : Nos utilisateurs étaient répartis à peu près à parts égales 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.
  • Mises à jour rapides, indépendamment 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 est plus facile d'installer 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 service worker. 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'avaient largement dépassé.

Générez-le si vous le pouvez

Évitez d'écrire manuellement le script d'un service worker. 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.

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 enseignements que nous en avons tirés ci-dessous peuvent parfois en tenir compte. Pour en savoir plus, consultez la section Pièges liés aux 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 reposent sur 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 un nœud de calcul de service. 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 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 pendant 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 IndexedDB (IDB) version N
  2. La nouvelle application Web est publiée avec la version N+1 de l'IDB
  3. L'utilisateur visite la PWA, ce qui déclenche le téléchargement d'un 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 de 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 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.

Résilience

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 reprendre à 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 en étions chaque enregistrement dans le processus.

Ne pas dépendre de l'état global

Étant donné que les service workers existent dans un contexte différent, de nombreux symboles auxquels vous vous attendez à exister 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 (journalisation, indicateurs, synchronisation, etc.). Le code doit être défensif quant aux 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. 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 effectuer cette opération via les outils pour les développeurs Chrome en cochant la case Ignorer pour le réseau (panneau Application > Service workers, volet Service workers), en plus de cocher la case Disable cache (Désactiver le cache) dans le panneau Network (Réseau) afin de désactiver le cache de la mémoire. Afin de couvrir davantage de navigateurs, nous avons opté pour une autre solution en incluant un indicateur permettant de désactiver la mise en cache dans notre service worker, activé par défaut dans les builds de développeur. Les développeurs bénéficient ainsi toujours de leurs modifications les plus récentes, sans 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. 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 sur l'intégration continue pour vous alerter si vous ne respectez pas l'un des critères d'intégration continue. Cela nous est arrivé une fois, lorsque le service worker n'était pas en cours d'installation et que nous ne l'avions pas réalisé avant le déploiement en production. Le fait d'avoir Lighthouse dans notre CI aurait permis d'éviter cela.

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. Lorsque l'utilisateur ouvrait notre application, le service worker diffusait l'ancien client pendant qu'il téléchargeait le nouveau 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, il recevra la nouvelle version du client la prochaine fois qu'il actualisera la page. 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 peu de temps de migration pour les clients. En règle générale, nous accordions aux utilisateurs un mois 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 n'avait pas ouvert l'application pendant une longue période. 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é par le fait de ne pas diffuser le contenu alors qu'il est obsolète ou de faire expirer manuellement le contenu après quelques semaines. Dans la pratique, nous n'avons jamais rencontré de problèmes de clients obsolètes. Le niveau de rigueur d'une équipe donnée dépend de son cas d'utilisation spécifique, mais les PWA offrent beaucoup plus de flexibilité que les 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 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 à des clients actifs (avec fenêtre) à partir du service worker pour demander les valeurs des cookies, bien qu'il soit possible qu'il s'exécute en arrière-plan sans qu'aucun client ne soit disponible, par exemple lors d'une synchronisation en arrière-plan. Pour contourner ce problème, nous avons créé sur notre serveur frontend un point de terminaison qui renvoie 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.

Pièges pour les service workers non générés

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

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 service workers ne sont installés que lorsque le navigateur détecte que leur script a été modifié d'une manière ou d'une autre. Nous avons donc dû nous assurer que le fichier de script du service worker a été modifié d'une manière ou d'une autre lorsqu'un fichier mis en cache a été modifié. Pour ce faire, nous avons procédé manuellement en intégrant un hachage de l'ensemble de fichiers de ressources statiques dans notre script de service worker. Ainsi, chaque version produit un fichier JavaScript de service worker distinct. Les bibliothèques de service workers telles que Workbox automatisent ce processus.

Tests unitaires

Les API Service Workers fonctionnent en ajoutant des écouteurs d'événements à l'objet global. Exemple :

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

Cela peut être fastidieux à 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 l'ensemble de 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 fait en sorte que le script du service worker principal soit aussi simple 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.

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

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