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 écran | Le manifeste | Un favori qui ouvre le navigateur |
| Fonctionnement hors ligne, démarrage instantané | Le service worker et son cache | « Vous êtes hors connexion » |
| Notifications push | Le service worker et l'API Push | Aucune |
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 :
{
"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" }
]
}
| Membre | Rôle |
|---|---|
name, short_name | Le nom complet (écran de démarrage, installation) et le nom court (sous l'icône) |
start_url | La page ouverte au lancement |
scope | Les adresses qui font partie de l'application ; une adresse hors de la portée s'ouvre dans le navigateur |
display | standalone : comme une application, sans barre d'adresse ; browser : un onglet normal ; fullscreen, minimal-ui |
background_color, theme_color | La couleur de l'écran de démarrage, et celle de la barre du système |
icons | Au 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 :
<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.
Le service worker d'AppliFrais, public/sw.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 :
install: la première fois, ou quand le fichiersw.jsa 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.waitUntilfait attendre le navigateur jusqu'à la fin du remplissage.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.fetch: à chaque requête de la page, le service worker choisit la réponse avecevent.respondWith. S'il n'appelle pasrespondWith, 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 :
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égie | Principe | Pour quoi |
|---|---|---|
| Cache d'abord (cache first) | La copie en cache si elle existe, le réseau sinon | Les 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 pas | Les 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 fois | Des données qui peuvent avoir un peu de retard : une liste de communes |
| Réseau seulement | Pas de cache | Les 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.
npm install --save-dev vite-plugin-pwa
// 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.
| Plateforme | Comment l'utilisateur installe |
|---|---|
| Android, Chrome | Une 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, Safari | Bouton 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 Edge | Une 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 ?
| Besoin | PWA | React Native (Expo) |
|---|---|---|
| Diffusion sans magasin, par une simple adresse | Oui | Non (APK à installer, ou magasin) |
| Mise à jour immédiate pour tous | Oui | Par le magasin ; mise à jour à distance possible pour le JavaScript |
| Fonctionnement hors ligne | Oui | Oui |
| Appareil photo, position | Oui, par les API du Web | Oui |
| Notifications push fiables sur iPhone | Seulement si installée sur l'écran d'accueil, iOS 16.4+ | Oui |
| Biométrie | Indirectement, par les passkeys (WebAuthn) | Oui, directement |
| Stockage sécurisé (trousseau, Keystore) | Non | Oui |
| Travail en arrière-plan, Bluetooth, NFC | Très limité, surtout sur iPhone | Oui |
| Situation « terminal mobile » de l'annexe II.E | Code exécuté dans le navigateur du téléphone : à argumenter | Oui, 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.
{
"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 fautstandalone(oufullscreen,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
namecomplet 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.
/assets/app-3f9a1c.js, généré par Vite.- La page
/fiches. POST https://api.gsb.example/v1/fiches/12/frais-hors-forfait.- La liste des 35 000 communes de France, mise à jour une fois par an.
GET https://api.gsb.example/v1/fiches, qui renvoie les fiches de Lucas.
Voir la réponseMasquer la réponse de l'exercice 2
- 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.
- Réseau d'abord, avec une page de secours hors ligne : la page doit être à jour quand le réseau est là.
- 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). - Copie puis mise à jour (ou cache d'abord avec une version) : la donnée change rarement, un léger retard est sans conséquence.
- 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é ?