Problématique des mises à jour d'applications web
Lorsqu'un utilisateur reste longtemps sur une application web, il est possible qu'une nouvelle version soit déployée sur le serveur sans qu'il ne s'en aperçoive. Sans un rafraîchissement manuel de la page, l'utilisateur continue d'interagir avec une version obsolète de l'application, ce qui peut entraîner des bugs, des fonctionnalités manquantes ou une expérience utilisateur dégradée. L'objectif est de mettre en place un mécanisme côté client pour détecter ces mises à jour en arrière-plan et inviter l'utilisateur à actualiser la page.
Principe de fonctionnement
La plupart des outils de build front-end (comme Webpack, Vite) génèrent des fichiers de ressources (JavaScript, CSS) avec des empreintes numériques (hashes) dans leur nom de fichier (par exemple, index.b3865953.js). Ces empreintes changent uniquement lorsque le contenu du fichier correspondant est modifié. Le principe est le suivant :
- Interroger régulièrement la page d'accueil du serveur pour récupérer le contenu HTML le plus récent.
- Extraire les chemins des fichiers JavaScript référencés par les balises
<script>dans ce HTML. - Comparer ces chemins avec ceux de la version actuellement chargée dans le navigateur.
- Si des différences sont détectées, cela indique une mise à jour. L'utilisateur est alors invité à rafraîchir la page.
Implémentation technique
1. Récupération et analyse des chemins de script
Nous utilisons l'API fetch pour récupérer le contenu HTML de la page d'accueil. Un paramètre de requête aléatoire (cacheBuster) est ajouté pour éviter que le navigateur ou un proxy ne serve une version mise en cache du HTML.
Voici un exemple de structrue HTML simplifiée que l'on pourrait récupérer :
<html lang="fr">
<head>
<meta charset="UTF-8">
<link rel="icon" href="./favicon.ico">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type="module" crossorigin src="./assets/index.b3865953.js"></script>
</head>
<body>
<div id="app"></div>
<script src="./js/utility.js" type="text/javascript"></script>
</body>
</html>
On remarque l'empreinte numérique b3865953 dans le chemin index.b3865953.js. C'est cette partie que nous allons surveiller.
La fonction recupererCheminsScripts se charge de cette tâche :
let cheminsScriptsActuels; // Stocke les chemins des scripts de la version active
const regexScripts = /<script\s+(?:[^>]*?\s+)?src=["'](.*?)(?=["'])/g; // Regex pour extraire les URL de 'src'
/**
* Récupère tous les chemins de fichiers JavaScript depuis le HTML de la page d'accueil.
* @returns {Promise<string[]>} Une promesse résolvant avec un tableau des chemins de script.
*/
async function recupererCheminsScripts() {
// Ajoute un paramètre pour forcer le rechargement de la ressource et éviter le cache
const reponseHtml = await fetch('/?cacheBuster=' + Date.now()).then((res) => res.text());
regexScripts.lastIndex = 0; // Réinitialise l'index du regex pour une nouvelle recherche
const scriptsTrouves = [];
let correspondance;
while ((correspondance = regexScripts.exec(reponseHtml))) {
scriptsTrouves.push(correspondance[1]);
}
return scriptsTrouves;
}
2. Vérification de la nécessité d'une mise à jour
Cette fonction compare les chemins de script de la version actuellement chargée (cheminsScriptsActuels) avec ceux nouvellement récupérés du serveur.
/**
* Vérifie si une mise à jour de l'application est nécessaire.
* @returns {Promise<boolean>} Une promesse résolvant à `true` si une mise à jour est détectée, `false` sinon.
*/
async function verifierPresenceMiseAJour() {
const nouveauxChemins = await recupererCheminsScripts();
if (!cheminsScriptsActuels) {
// Si c'est la première vérification, initialise avec les chemins actuels
cheminsScriptsActuels = nouveauxChemins;
return false; // Pas de mise à jour à l'initialisation
}
// Compare la longueur des tableaux de chemins
if (cheminsScriptsActuels.length !== nouveauxChemins.length) {
cheminsScriptsActuels = nouveauxChemins; // Met à jour pour la prochaine vérification
return true; // Nombre de scripts différent, une mise à jour est probable
}
// Compare chaque chemin individuel
for (let i = 0; i < cheminsScriptsActuels.length; i++) {
if (cheminsScriptsActuels[i] !== nouveauxChemins[i]) {
cheminsScriptsActuels = nouveauxChemins; // Met à jour pour la prochaine vérification
return true; // Un chemin de script a changé
}
}
cheminsScriptsActuels = nouveauxChemins; // Met à jour même si pas de changement
return false; // Aucune différence détectée
}
3. Notification de l'utilisateur et rafraîchissement
La fonction demarrerSurveillanceMisesAJour orchestre le processus en appelant verifierPresenceMiseAJour à intervalles réguliers. Si une mise à jour est détectée, elle affiche une boîte de dialogue à l'utilisateur.
const INTERVALLE_VERIFICATION_MS = 5000; // Intervalle de vérification en millisecondes (ici, 5 secondes)
/**
* Démarre la surveillance des mises à jour de l'application.
* Appelle `verifierPresenceMiseAJour` périodiquement et notifie l'utilisateur si une mise à jour est disponible.
*/
function demarrerSurveillanceMisesAJour() {
setTimeout(async () => {
const miseAJourDetectee = await verifierPresenceMiseAJour();
if (miseAJourDetectee) {
const confirmation = confirm("Une nouvelle version de l'application est disponible. Souhaitez-vous la recharger maintenant ?");
if (confirmation) {
location.reload(); // Recharge la page actuelle
}
// Si l'utilisateur refuse, on continue de surveiller, mais il restera sur l'ancienne version.
// Une autre stratégie pourrait être de forcer le rafraîchissement après un certain temps ou nombre de refus.
}
// Continue la surveillance, indépendamment du résultat précédent
demarrerSurveillanceMisesAJour();
}, INTERVALLE_VERIFICATION_MS);
}
// Lance la surveillance dès que l'application est chargée
demarrerSurveillanceMisesAJour();
En intégrant ce code JavaScript dans le point d'entrée principal de votre application (par exemple, main.js ou app.js), vous assurez une détection proactive des mises à jour, améliorant ainsi l'expérience utilisateur et la cohérence de l'application.