Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1018 minIntermédiaire

PWA : manifest, service worker et installation

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • dire ce qu'est une PWA et ce qu'il lui faut ;
  • écrire un manifeste valide et un service worker ;
  • choisir une stratégie de cache pour chaque ressource ;
  • comparer une PWA et une application React Native pour un besoin.

3 exercices corrigés1 schéma

Une PWA (Progressive Web App, application web progressive) est un site web qui s'installe sur l'écran d'accueil d'un téléphone et fonctionne comme une application : il s'ouvre en plein écran, sans barre d'adresse, et reste utilisable hors ligne. Il lui faut trois ingrédients : être servi en HTTPS, avoir un manifeste qui décrit l'application, et un service worker, un script qui intercepte les requêtes pour servir des fichiers en cache. Ce chapitre transforme la version web d'AppliFrais, le front React et TypeScript construit avec Vite dans le cours React (build et déploiement), en PWA installable, puis compare cette solution à l'application React Native.

C'est quoi une PWA ?

Une PWA n'est pas une technologie à part : c'est un site web ordinaire, en HTML, CSS et JavaScript, auquel on ajoute deux fichiers. Elle garde tous les avantages du Web (une adresse suffit, aucune compilation, aucun magasin, une mise à jour visible au prochain chargement) et gagne trois capacités d'application :

CapacitéGrâce àSans PWA
Installation sur l'écran d'accueil, ouverture en plein écranLe manifesteUn favori qui ouvre le navigateur
Fonctionnement hors ligne, démarrage instantanéLe service worker et son cache« Vous êtes hors connexion »
Notifications pushLe service worker et l'API PushAucune

Le terme a été proposé en 2015 par Alex Russell, ingénieur chez Google, et la designer Frances Berriman. Les éléments techniques, eux, sont des standards du W3C, pris en charge par tous les navigateurs modernes, avec des différences sur iPhone.

Le manifeste

Le manifeste est un fichier JSON qui décrit l'application installée : son nom, ses icônes, sa couleur, la page de démarrage et le mode d'affichage. Dans un projet Vite, il se place dans le dossier public/, qui est servi tel quel :

json
{
  "name": "AppliFrais – Notes de frais GSB",
  "short_name": "AppliFrais",
  "description": "Saisie et suivi des fiches de frais des visiteurs médicaux",
  "lang": "fr",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "orientation": "portrait",
  "background_color": "#ffffff",
  "theme_color": "#1d4ed8",
  "icons": [
    { "src": "/icones/icone-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icones/icone-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icones/icone-masquable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}
MembreRôle
name, short_nameLe nom complet (écran de démarrage, installation) et le nom court (sous l'icône)
start_urlLa page ouverte au lancement
scopeLes adresses qui font partie de l'application ; une adresse hors de la portée s'ouvre dans le navigateur
displaystandalone : comme une application, sans barre d'adresse ; browser : un onglet normal ; fullscreen, minimal-ui
background_color, theme_colorLa couleur de l'écran de démarrage, et celle de la barre du système
iconsAu moins une icône de 192 × 192 et une de 512 × 512 pixels ; une icône maskable a des marges suffisantes pour être découpée en cercle ou en carré arrondi par Android

Le manifeste est déclaré dans le <head> de index.html, avec quelques balises pour iOS :

html
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#1d4ed8" />
<link rel="apple-touch-icon" href="/icones/icone-192.png" />

Le service worker

Un service worker est un script JavaScript que le navigateur exécute à part de la page, en arrière-plan. Il se place entre la page et le réseau : chaque requête de la page (une page, un script, une image, un appel d'API) passe par son événement fetch, et il décide de la réponse : celle du réseau, une copie en cache, ou une page de secours.

PageAppliFrais Web/assets/app.js/fichesPOST, APIsw.jsévénement fetchfichiers statiques :le cache d'abordpages : réseau d'abordsinon hors-ligne.htmlPOST et autre origine :pas d'interceptionCache Storageapplifrais-v3coquille, JS, CSSRéseauserveur web, APIinstall : met la coquille en cacheactivate : supprime les anciens cachespuis fetch à chaque requête ; changer VERSION à chaque déploiement relance le cycle
Le service worker intercepte chaque requête : cache d'abord pour les fichiers, réseau d'abord pour les pages, rien pour l'API

Le service worker d'AppliFrais, public/sw.js :

js
// sw.js — le service worker d'AppliFrais Web
const VERSION = 'applifrais-v3';
const COQUILLE = ['/', '/index.html', '/hors-ligne.html', '/icones/icone-192.png'];

// 1. Installation : on met en cache la coquille de l'application.
self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(VERSION).then((cache) => cache.addAll(COQUILLE)));
});

// 2. Activation : on supprime les caches des versions précédentes.
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cles) => Promise.all(cles.filter((cle) => cle !== VERSION).map((cle) => caches.delete(cle)))),
  );
});

// 3. Chaque requête de la page passe par ici.
self.addEventListener('fetch', (event) => {
  const requete = event.request;
  if (requete.method !== 'GET') return; // POST, PUT, DELETE : toujours le réseau
  const url = new URL(requete.url);

  if (url.origin !== self.location.origin) return; // l'API (autre origine) n'est jamais mise en cache

  if (requete.mode === 'navigate') {
    // Les pages : le réseau d'abord, la page hors ligne si le réseau ne répond pas.
    event.respondWith(fetch(requete).catch(() => caches.match('/hors-ligne.html')));
    return;
  }

  // Les fichiers statiques (JS, CSS, images) : le cache d'abord, le réseau sinon.
  event.respondWith(
    caches.match(requete).then(
      (enCache) =>
        enCache ??
        fetch(requete).then((reponse) => {
          if (reponse.ok) {
            const copie = reponse.clone(); // une réponse ne se lit qu'une fois
            caches.open(VERSION).then((cache) => cache.put(requete, copie));
          }
          return reponse;
        }),
    ),
  );
});

Le cycle de vie d'un service worker a trois étapes :

  1. install : la première fois, ou quand le fichier sw.js a changé d'un seul octet, le navigateur installe la nouvelle version. On en profite pour mettre en cache la coquille : les fichiers indispensables pour afficher quelque chose sans réseau. event.waitUntil fait attendre le navigateur jusqu'à la fin du remplissage.
  2. activate : la nouvelle version prend la main, une fois que plus aucun onglet n'utilise l'ancienne. On supprime alors les caches des versions précédentes : c'est pour cela que le nom du cache contient une version (applifrais-v3), à changer à chaque déploiement.
  3. fetch : à chaque requête de la page, le service worker choisit la réponse avec event.respondWith. S'il n'appelle pas respondWith, la requête suit son cours normal, sur le réseau.

L'enregistrement se fait depuis le code de la page, par exemple dans src/main.tsx de l'application React :

ts
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js').catch((e) => console.error('Service worker non enregistré', e));
  });
}

Le service worker ne fonctionne qu'en HTTPS (et sur localhost, pour le développement), parce qu'il intercepte tout le trafic de la page : sur une connexion en clair, un intermédiaire pourrait installer le sien. Sa portée est le dossier où il se trouve : placé à la racine, /sw.js contrôle tout le site.

Les stratégies de cache

Chaque type de ressource appelle une stratégie différente :

StratégiePrincipePour quoi
Cache d'abord (cache first)La copie en cache si elle existe, le réseau sinonLes fichiers qui ne changent pas sans changer de nom : JS et CSS de Vite (leur nom contient une empreinte, app-3f9a1c.js), icônes, polices
Réseau d'abord (network first)Le réseau ; la copie en cache s'il ne répond pasLes pages, les données qui doivent être fraîches
Copie, puis mise à jour (stale-while-revalidate)La copie tout de suite, et le réseau en arrière-plan pour la prochaine foisDes données qui peuvent avoir un peu de retard : une liste de communes
Réseau seulementPas de cacheLes requêtes qui modifient (POST, PUT, DELETE), les données personnelles

Avec Vite : vite-plugin-pwa

Écrire le service worker à la main est le meilleur moyen de le comprendre. Dans un vrai projet, le greffon vite-plugin-pwa (version 2) le génère au moment du vite build : il produit le manifeste, la liste exacte des fichiers à mettre en cache avec leurs empreintes, et le code de mise à jour, à partir de la bibliothèque Workbox de Google.

bash
npm install --save-dev vite-plugin-pwa
ts
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'autoUpdate',
      manifest: {
        name: 'AppliFrais – Notes de frais GSB',
        short_name: 'AppliFrais',
        lang: 'fr',
        theme_color: '#1d4ed8',
        icons: [
          { src: '/icones/icone-192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icones/icone-512.png', sizes: '512x512', type: 'image/png' },
        ],
      },
    }),
  ],
});

Installer la PWA

Les navigateurs ne proposent l'installation que si le site remplit des conditions : servi en HTTPS, un manifeste avec un nom, des icônes de 192 et 512 pixels, une start_url et un mode d'affichage standalone, fullscreen ou minimal-ui.

PlateformeComment l'utilisateur installe
Android, ChromeUne invitation « Installer l'application » apparaît, ou le menu ⋮ → « Installer l'application ». La PWA reçoit une icône, une entrée dans la liste des applications, et s'ouvre en plein écran
iPhone, SafariBouton Partager → « Sur l'écran d'accueil ». Depuis iOS 26, tout site ajouté à l'écran d'accueil s'ouvre par défaut comme une application web
Ordinateur, Chrome ou EdgeUne icône d'installation dans la barre d'adresse

Sur Chrome et les navigateurs de la même famille, la page peut proposer son propre bouton « Installer » : elle intercepte l'événement beforeinstallprompt, le garde, et l'utilise au clic. Cet événement n'existe ni dans Safari ni dans Firefox : sur iPhone, il faut expliquer à l'utilisateur le chemin Partager → « Sur l'écran d'accueil ».

Les notifications push dans une PWA

Une PWA peut recevoir des notifications push avec l'API Push du Web : le service worker reçoit un événement push et affiche la notification avec self.registration.showNotification(). Le serveur envoie les messages avec le protocole Web Push, signés par une paire de clés VAPID. Sur iPhone, ces notifications ne fonctionnent que depuis iOS 16.4, et seulement si la PWA a été ajoutée à l'écran d'accueil : un utilisateur qui reste dans Safari ne les reçoit pas. C'est l'une des raisons qui font préférer une application installée quand les notifications sont essentielles.

Tester une PWA

Les outils de développement de Chrome (F12) ont un onglet Application fait pour cela :

  • Manifest : affiche le manifeste lu, ses erreurs et l'aperçu des icônes ;
  • Service workers : l'état du service worker, le bouton Update, la case Update on reload (recharger la nouvelle version à chaque fois, pratique en développement) et la case Offline, qui simule l'absence de réseau ;
  • Cache storage : le contenu de chaque cache ;
  • Storage → Clear site data : tout effacer pour repartir de zéro.

Le scénario de test minimal : charge l'application, coche Offline, recharge : la coquille doit s'afficher, et une page non mise en cache doit montrer hors-ligne.html. Puis modifie VERSION, recharge, et vérifie dans Cache storage que l'ancien cache a disparu.

PWA ou application React Native ?

BesoinPWAReact Native (Expo)
Diffusion sans magasin, par une simple adresseOuiNon (APK à installer, ou magasin)
Mise à jour immédiate pour tousOuiPar le magasin ; mise à jour à distance possible pour le JavaScript
Fonctionnement hors ligneOuiOui
Appareil photo, positionOui, par les API du WebOui
Notifications push fiables sur iPhoneSeulement si installée sur l'écran d'accueil, iOS 16.4+Oui
BiométrieIndirectement, par les passkeys (WebAuthn)Oui, directement
Stockage sécurisé (trousseau, Keystore)NonOui
Travail en arrière-plan, Bluetooth, NFCTrès limité, surtout sur iPhoneOui
Situation « terminal mobile » de l'annexe II.ECode exécuté dans le navigateur du téléphone : à argumenterOui, sans discussion

Pour un outil interne simple, utilisé ponctuellement, une PWA est souvent le meilleur rapport effort-résultat : ton application React existante devient installable en une après-midi. Dès qu'il faut des notifications fiables, un stockage sécurisé ou le matériel du téléphone, l'application native l'emporte. Pour l'E6, une PWA ajoutée à ton application web montre une compétence réelle, mais elle reste du code exécuté dans un navigateur : si tu comptes sur elle pour la situation « terminal mobile », vérifie ce point avec ton enseignant.

Exercice 1 sur 3 Corriger le manifeste Réussi À revoir

Chrome refuse d'installer ce site. Trouve les problèmes.

json
{
  "short_name": "Frais",
  "start_url": "/",
  "display": "browser",
  "icons": [{ "src": "/icone.png", "sizes": "64x64", "type": "image/png" }]
}
Voir la réponseMasquer la réponse de l'exercice 1
  • "display": "browser" : la page s'ouvrirait comme un onglet ordinaire. Il faut standalone (ou fullscreen, minimal-ui).
  • Une seule icône de 64 × 64 : il faut au moins une icône de 192 × 192 et une de 512 × 512.
  • Vérifier aussi le reste des conditions : un name complet est recommandé, le site doit être servi en HTTPS et déclarer le manifeste avec <link rel="manifest">.

Tu avais trouvé ?

Exercice 2 sur 3 Quelle stratégie de cache ? Réussi À revoir

Pour chaque ressource d'AppliFrais Web, choisis : cache d'abord, réseau d'abord, copie puis mise à jour, ou réseau seulement.

  1. /assets/app-3f9a1c.js, généré par Vite.
  2. La page /fiches.
  3. POST https://api.gsb.example/v1/fiches/12/frais-hors-forfait.
  4. La liste des 35 000 communes de France, mise à jour une fois par an.
  5. GET https://api.gsb.example/v1/fiches, qui renvoie les fiches de Lucas.
Voir la réponseMasquer la réponse de l'exercice 2
  1. Cache d'abord : le nom contient une empreinte du contenu ; s'il change, le nom change aussi, donc une copie en cache est toujours juste.
  2. Réseau d'abord, avec une page de secours hors ligne : la page doit être à jour quand le réseau est là.
  3. Réseau seulement : une requête qui modifie ne se met jamais en cache (le service worker de l'exemple l'ignore, car ce n'est pas un GET).
  4. Copie puis mise à jour (ou cache d'abord avec une version) : la donnée change rarement, un léger retard est sans conséquence.
  5. Réseau seulement dans le service worker : ce sont des données personnelles, qui ne doivent pas rester dans le cache du navigateur. Si l'application doit les afficher hors ligne, elle les garde elle-même, dans IndexedDB, et les efface à la déconnexion.

Tu avais trouvé ?

Exercice 3 sur 3 La nouvelle version qui n'arrive pas Réussi À revoir

Après un déploiement, les utilisateurs de la PWA voient toujours l'ancienne interface, même en rechargeant. Le développeur a modifié App.tsx mais n'a touché ni à sw.js ni à ses noms de fichiers. Explique, et propose deux corrections.

Voir la réponseMasquer la réponse de l'exercice 3

Avec la stratégie cache d'abord, le service worker renvoie la copie en cache des fichiers dont le nom n'a pas changé ; et comme sw.js n'a pas changé d'un octet, le navigateur n'installe pas de nouvelle version, donc l'événement activate ne vide pas l'ancien cache. Deux corrections, à combiner : faire produire par l'outil de construction des noms de fichiers avec empreinte (app-3f9a1c.js, ce que Vite fait par défaut pour ses fichiers dans assets/), et changer VERSION dans sw.js à chaque déploiement, pour que le nouveau service worker s'installe et supprime l'ancien cache. vite-plugin-pwa automatise les deux.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Tester une application mobile : Jest, émulateur et téléphoneRevoir le sommaire du cours