Les PWA sont un sujet de plus en plus présent, les éloges ne manquent pas. On serait presque tenté de se lancer tête baissée dans les PWA et jeter nos vieilles applications natives / hybrides. Nous n’y sommes pas (encore). Mais les PWA représentent bien le futur du Web, avec de nombreux bénéfices déjà accessibles. Voyons comment nous pouvons mettre en œuvre une PWA simple pour mieux visualiser les bénéfices.

Notez qu’à ce jour, Safari (donc iOS) supporte encore assez mal les PWA (même s’il a un premier niveau de support depuis peu). Mais étant progressive, l’application est toujours accessible au format Web avec l’essentiel de ses fonctionnalités, et Safari permet tout de même de créer un raccourci vers l’écran d’accueil avec le mode hors-ligne.

Objectif

Un certain nombre de tutoriels existent sur les PWA, mais la plupart présentent comment faire une PWA sans framework, ou comment coder ses services workers à la main. Cet article présente une PWA React déployable en production tout en se limitant à l’essentiel pour une mise en place rapide.

À la fin de ce tutoriel, vous aurez déployé une PWA accessible hors ligne, installée sur votre écran d’accueil Android, avec les mises à jour chargées et l’utilisateur informé qu’une mise à jour est disponible.

Une démo de ce que nous allons produire est déployée ici, et le code source est disponible .

Pré-requis

  • Nodejs, version >= 8 (lien)
  • Un device Android pour tester la PWA
  • Un IDE de votre choix (VS Code, WebStorm…), ou au minimum un éditeur de code type Notepad++
  • Savoir ouvrir un terminal (PowerShell ou “cmd” sur Windows) et exécuter quelques commandes

Commençons !

Ouvrez un terminal, allez dans le répertoire qui contiendra le dossier de votre projet, et exécutez :

npx create-react-app my-app

Patientez un peu…

Un dossier “my-app” a été créé.

cd my-app

Testons le projet généré.

npm start

Patientez un peu… puis vous verrez dans votre navigateur :

Et voilà, vous avez votre première PWA.

“Euh, c’est tout ? Je croyais qu’il fallait coder quelque chose.”

C’est tout pour la PWA simple qui offre un support hors-ligne ! Nous avons utilisé ici l’outil “create-react-app” qui génère un squelette d’application React. Rassurez-vous, vous allez devoir coder un peu pour bénéficier des autres avantages d’une PWA et améliorer l’expérience utilisateur 😊

Tester notre première application hors ligne

Pour ceux qui s’attendaient à voir fonctionner le site hors ligne, petite déception : quand on coupe le serveur de développement (Ctrl + C après npm start) et rafraîchit la page, le site ne s’affiche plus ! :’(

Explication : le cache du mode hors-ligne serait très pénible pour les développements, car le nouveau code ne serait pas tout de suite rechargé. Il n’est donc activé que pour les builds de production.

Testons donc notre build de production.

npm run build

L’app de production est dans le dossier “build”. Maintenant, servons-la dans le navigateur.

npx http-server build -o -a localhost

Vous verrez à nouveau l’application dans le navigateur. Retournez dans le terminal, arrêtez le serveur (Ctrl + C), puis rafraîchissez la page dans le navigateur. Cette fois, l’application reste accessible !

La technologie utilisée derrière pour arriver à ce résultat est le “service worker”, une composante très importante des PWA. Les applications sont vastes et complexes, donc nous ne rentrerons pas dans les détails. Sachez que pour le scénario qui nous intéresse ici, l’implémentation est déjà fournie par le modèle du projet. Pour le faire vous-même sur un projet utilisant un autre framework, vous pouvez vous intéresser aux outils du type sw-precache et sw-toolbox qui fourniront l’implémentation correspondant à la configuration fournie. C’est aussi une manière de supporter d’autres stratégies de cache.

Tester sur mobile ou tablette

Allons jusqu’au bout et déployons l’application sur un vrai serveur pour pouvoir tester facilement sur un device et partager avec ses amis. Pour cela, nous allons utiliser l’hébergement Firebase qui a un quota gratuit.

Ouvrez https://console.firebase.google.com et connectez-vous avec votre compte Google. Créez un projet.

Suivez les étapes. Une fois le projet créé, ouvrez la section “Hosting” depuis le menu à gauche.

Cliquez sur “Premiers pas”.

Suivez les étapes indiquées. Exécutez dans le terminal :

npm install -g firebase-tools
firebase login

Vous serez redirigé vers une page Google pour l’authentification la première fois. Suivez les étapes.

firebase init

Sélectionnez le service « Hosting ».

Sélectionnez votre application nouvellement créée dans la liste.

Entrez “build” comme dossier public.

Activez le mode single-page app.

N’écrasez pas le fichier index.html existant.

Une fois la configuration générée, il reste une chose à faire avant de déployer. Faites une modification dans le fichier “firebase.json” fraîchement généré pour éviter un problème de cache d’un fichier-clé des PWA. Ajoutez le contenu en gras ci-dessous.

{
  "hosting": {
    "public": "build",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ],
    "headers": [
      {"source": "/service-worker.js", "headers": [{"key": "Cache-Control", "value": "no-cache"}]}
    ]
  }
}
npm run build

(si ça n’a pas déjà été fait à l’étape précédente)

firebase deploy

Visitez l’URL indiquée (aussi accessible depuis la console Firebase). Vous avez maintenant accès à votre PWA depuis n’importe quel device ! Entrez l’URL sur votre device pour charger la page. Activez ensuite le mode avion et rafraîchissez la page. Et voilà, votre application Web est accessible hors ligne !

Note : Après avoir apporté des modifications, pour redéployer, ré-exécutez les commandes “npm run build” puis “firebase deploy” depuis le dossier du projet.

Installation

Une autre particularité des PWA est qu’elles peuvent être “installées”, c’est-à-dire avoir une icône de démarrage sur la page d’accueil du device, à côté des app natives. C’est comparable à faire un simple raccourci vers l’application Web, mais avec le fonctionnement hors ligne et quelques petits bonus, notre application Web est très similaire à une application native.

Essayons ça. Pour que Chrome reconnaisse notre PWA, il reste encore un critère à remplir sur notre application React (liste ici) : une icône à fournir. Téléchargez et placez les deux images suivantes dans le dossier public/assets (dossier à créer).

Modifiez le fichier public/manifest.json : ajoutez le contenu entre les lignes ** ci-dessous.

{
  "short_name": "React App",
  "name": "Create React App Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    ***************************************
    {
      "src": "assets/logo-512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "assets/logo-192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
    ***************************************
  ],
  "start_url": "./index.html",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

Re-déployez ensuite l’application.

npm run build
firebase deploy

Videz le cache de Chrome pour supprimer le cache de “service worker”, afin de tester le chargement initial du site : Menu > Paramètres > Confidentialité > Effacer les données de navigation > cochez “Cookies et données de site” et validez.

Rechargez ensuite l’application sur votre device. Vous devriez maintenant avoir une suggestion d’installation de l’application.

Cliquez sur “Ajouter à l’écran d’accueil”. Vous aurez maintenant l’icône de votre application sur l’écran d’accueil.

Si vous l’ouvrez, vous retrouverez votre application, toujours accessible hors ligne, en plein écran (sans la barre de navigation de Chrome). Ca commence sérieusement à ressembler à une application prise sur un Store. (Bon, OK, il faudrait un peu plus de fonctionnalités !)

Icônes pour iOS et Windows

La configuration en place fonctionne sur Chrome (donc Android), mais nous pouvons ajouter un petit complément pour que iOS et Windows détectent correctement l’icône et les couleurs (icône sur l’écran d’accueil, dans les favoris…).

Ouvrez public/index.html et ajoutez le code entre les deux lignes de **.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    ***********************************************************
    <!-- Add to home screen for Safari on iOS -->
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="apple-mobile-web-app-title" content="React App">
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/assets/logo-192.png">
    <!-- Windows Tile Icon -->
    <meta name="msapplication-TileImage" content="%PUBLIC_URL%/assets/logo-192.png">
    <meta name="msapplication-TileColor" content="#000000">
    ***********************************************************
    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">

    <!--
    …
    -->

Gestion des mises à jour

L’approche actuellement en place est “cache-first”, ce qui signifie que le contenu trouvé en cache est utilisé en priorité. C’est pratique et rapide, mais il faut bien penser à ce qu’il se passera quand du nouveau contenu sera déployé. L’approche implémentée dans ce projet est la suivante :

Lors du premier chargement :

  1. L’application est chargée normalement, comme un site classique. Mais les fichiers chargés sont mis en cache.
  2. Une fois la mise en cache terminée, l’application est informée qu’elle est maintenant accessible hors ligne.

Lors des prochains chargements :

  1. Lors du chargement d’une page, le contenu déjà en cache est chargé.
  2. En arrière-plan, la dernière version est chargée.
  3. Une fois la nouvelle version complètement chargée, l’application est informée que du nouveau contenu est disponible.

Les notifications sont déjà en place sous forme de logs. Notifions maintenant l’utilisateur en faisant le pont avec l’application React.

Pour rendre l’évènement disponible à l’application, ouvrez src/registerServiceWorker.js et assurez-vous d'avoir tous les bouts de code ci-dessous.

const isLocalhost = Boolean(
  // ....
);

// Creates a super minimalist observable to notify the app when updates are available.
function O(){this.a=[]}O.prototype={subscribe:function(a){a&&(this.a.push(a),void 0!==this.b&&a(this.b))},next:function(a){this.b=a;for(var b=0,d=this.a;b<d.length;b++)(0,d[b])(a)}};
var obs = new O();
window.swObservable = obs;

// ....

function registerValidSW(swUrl, obs) {
  // ....
          if (installingWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              console.log('New content is available; please refresh.');
              obs.next(true);
            } else {
              console.log('Content is cached for offline use.');
              obs.next(false);
            }
          }
  // ....
}

Nous venons de créer un observable minimaliste auquel l’application peut souscrire. Pour récupérer ensuite l’évènement, ouvrez src/App.js et ajoutez le code qu'il vous manque pour avoir le code ci-dessous.

import React, { Component } from 'react';
import './App.css';
import logo from './logo.svg';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { hasUpdate: undefined };
  }

  componentWillMount() {
    if (window.swObservable) {
      window.swObservable.subscribe(hasUpdate => this.setState({ hasUpdate }));
    }
  }

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo"/>
          <h1 className="App-title">Welcome to React 2</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        {this.state.hasUpdate !== undefined && (this.state.hasUpdate ? (
          <p className="sw-notification">Une mise à jour est disponible. Veuillez rafraîchir l'application.</p>
        ) : (
          <p className="sw-notification">L'application est maintenant en cache et prête à être utilisée hors ligne.</p>
        ))}
      </div>
    );
  }
}

export default App;

Un petit peu de CSS pour la mise en forme. Dans src/App.css, ajoutez le code ci-dessous.

.sw-notification {
  display: inline-block;
  padding: 10px 20px;
  background-color: #eee;
  border: solid 1px lightgray;
  border-radius: 4px;
}

Pour tester la notification, vous pouvez ajouter temporairement l’une des lignes de code suivantes à l’emplacement de votre choix (par exemple le constructeur du composant).

// window.swObservable.next(true);
// window.swObservable.next(false);

Il est temps de déployer et de tester.

npm run build
firebase deploy

Vous pouvez maintenant tester :

  1. Videz le cache de Chrome pour supprimer le cache de “service worker”, afin de tester le chargement initial du site : Menu > Paramètres > Confidentialité > Effacer les données de navigation > cochez “Cookies et données de site” et validez.
  2. Ouvrez votre application. Vous aurez alors un message indiquant que l’application est mise en cache.
  3. Rafraîchissez (pull-to-refresh, y compris avec la version installée) : le message disparaît, et le site reste accessible hors-ligne (par exemple en mode avion).
  4. Faites un changement dans le code et re-déployez.
  5. Rechargez l’application : un message vous indiquera qu’une nouvelle version est disponible, et qu’un rechargement est nécessaire pour utiliser la dernière version.
  6. Rechargez encore et vous aurez la dernière version, avec vos changements.

À l’étape 5, vous n’avez pas encore la dernière version car, au moment du chargement, elle n’a pas encore été téléchargée, et la version en cache est utilisée en priorité. Mais, en tâche de fond, cette nouvelle version est chargée et, une fois entièrement disponible, le message indique qu’elle est prête à être utilisée. Sur une grosse application où les délais de téléchargement sont importants, la différence sera notable, et l’expérience utilisateur en sera grandement améliorée.

Auditer l’application avec Lighthouse

L’outil de Chrome Lighthouse permet de lancer une batterie de tests pour vérifier la performance de notre application, le respect des bonnes pratiques, etc. Voyons ce que ça donne pour notre application déployée.

  • Ouvrez Chrome DevTools (F12 ou Ctrl + Maj + I)
  • Allez dans l’onglet Audits et lancer l’audit.

Nous obtenons de bons résultats pour la catégorie PWA.

Les critères sont susceptibles de varier au cours du temps, donc vous obtiendrez probablement des notes différentes. Lancer un audit fréquent est un bon moyen de mesurer la qualité du site déployé. Ce n’est certainement pas exhaustif, mais c’est un bon début.

Notifications push

Les technologies Web utilisées dans les PWA (service workers) permettent d’utiliser les notifications push, que l’on croit souvent (à tort) être une exclusivité des applications mobiles natives. Les mettre en place dans notre prototype prendra un peu plus de temps et create-react-app n’a pas de support simple. Nous les gardons donc pour un prochain article.

Conclusion

Nous avons produit une PWA qui offre une bonne expérience utilisateur. Ces fonctionnalités contribuent à accélérer le chargement de l’application et à rendre la navigation plus fluide. Ce code est déployable en production : il peut être intégré à une application existante ou servir de base à la création d’une nouvelle application. Gardez bien en tête que c’est un scénario standard qui est couvert ici, à adapter aux usages de l’application.

Pour aller plus loin

  • Un outil comme cra-append-sw permet d’ajouter son propre code de service worker, ce qui ouvre la voie des notifications push sur un projet create-react-app sans « eject ».
  • Il est possible de contrôler un petit peu plus finement la pop-up de demande d’installation. Dans des versions à venir, Chrome mettra également plus en évidence l’installation des PWA (note ici). Restez à l’écoute des changements pour la meilleure façon de faire installer votre PWA !
  • Il y a quelques différences avec les applications natives. Il y a des avantages, mais aussi des fonctionnalités manquantes (fingerprint, accès au carnet d’adresse…). Plus de détails ici.
  • Cet article utilise la commande firebase globale, mais il est recommandé d’installer les modules des CLI en “devDependencies” (et non global). De même pour tout ce dont vous avez besoin pour le développement de ce projet, disponible sous forme de module Node. Cette approche offre une meilleure portabilité. Et ces outils en ligne de commande sont accessibles dans les scripts npm comme ils le seraient dans un terminal si installés globalement. Vous pouvez vérifier le fichier « package.json » de cet exemple pour illustrer ça. Après avoir exécuté npm install, essayez de désinstaller firebase-tools globalement (npm uninstall -g firebase-tools), puis d’exécuter npm run deploy (qui inclut le build et le déploiement firebase).