Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

L'API History et le principe des applications monopage

Comprenez le fonctionnement des applications monopage (SPA) : différence avec un site classique, API History (pushState, replaceState, popstate), écriture d'un mini-routeur en JavaScript avec routes à...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Comprenez le fonctionnement des applications monopage (SPA) : différence avec un site classique, API History (pushState, replaceState, popstate), écriture d'un mini-routeur en JavaScript avec routes à paramètres, configuration du serveur (Nginx, Apache), routage par hash, et découverte de la Navigation API, Baseline depuis janvier 2026. Avec des activités corrigées.
  1. Objectifs

    • Comprendre la différence entre un site classique et une application monopage (SPA).
    • Manipuler l'historique du navigateur avec pushState(), replaceState() et l'événement popstate.
    • Écrire un petit routeur en JavaScript et configurer le serveur qui l'héberge.
    • Découvrir la Navigation API, la remplaçante moderne de l'API History.
  2. Site classique et application monopage

    • Dans un site classique (multi-pages), chaque clic sur un lien demande une nouvelle page HTML au serveur : la page entière est rechargée, l'écran blanchit un instant et l'état de JavaScript (variables, formulaire en cours) est perdu.
    • Dans une application monopage (Single Page Application, SPA), le navigateur charge une seule page HTML. Ensuite, JavaScript intercepte les clics et remplace seulement le contenu qui change. Les données manquantes sont demandées avec fetch.
    • Avantages : navigation instantanée, transitions fluides, état conservé (lecteur audio qui continue, panier…). Inconvénients : premier chargement plus lourd, référencement et accessibilité à soigner.
    • C'est le fonctionnement d'Angular, React et Vue (chapitre 24), de Gmail ou de YouTube. Leurs routeurs (Angular Router, React Router, Vue Router) reposent sur ce que vous allez apprendre ici.

    Site classique et application monopage, pile de l'historique avec pushState et popstate

  3. Le défi : garder des adresses qui fonctionnent

    • Si l'on se contente de changer le contenu avec JavaScript, l'adresse ne bouge pas. Résultat : impossible de mettre une page en favori, de partager un lien, ou d'utiliser les boutons Précédent et Suivant.
    • L'API History (objet history) permet de modifier l'adresse et l'historique sans recharger la page.
  4. Les outils de l'API History

  5. InstructionEffet
    history.pushState(etat, "", url)Ajoute une entrée à l'historique et affiche url dans la barre d'adresse, sans recharger ni contacter le serveur.
    history.replaceState(etat, "", url)Remplace l'entrée actuelle (pratique pour mettre à jour un filtre dans l'adresse sans créer une entrée à chaque frappe).
    history.stateL'objet etat associé à l'entrée actuelle.
    history.back(), forward(), go(-2)Équivalent des boutons du navigateur.
    Événement popstate (sur window)Déclenché quand l'utilisateur utilise Précédent / Suivant; event.state contient l'objet enregistré.
  • Le 2e paramètre (titre) est ignoré par les navigateurs : on passe "".
  • L'url doit être de la même origine (même domaine) que la page.
  • pushState() ne déclenche pas popstate: c'est à vous d'afficher le nouveau contenu.
  • etat doit rester petit et « clonable » (nombres, textes, objets simples) : pas de fonctions ni d'éléments du DOM.
  1. Exemple complet : un mini-routeur

  2. Le site d'un centre de formation avec quatre types de pages : accueil, liste des formations, fiche d'une formation (/formations/2) et contact, plus une page 404.

    index.html:

    <!doctype html>
    <html lang="fr">
      <head>
        <meta charset="utf-8" />
        <title>Centre de formation</title>
        <script type="module" src="/routeur.js"></script>
      </head>
      <body>
        <nav>
          <a href="/" data-lien>Accueil</a> |
          <a href="/formations" data-lien>Formations</a> |
          <a href="/contact" data-lien>Contact</a>
        </nav>
        <main id="app"></main>
      </body>
    </html>

    routeur.js:

    const app = document.getElementById("app");
    
    const formations = ["Développement web", "Développement mobile", "Réseaux"];
    
    // Une "page" = une fonction qui construit le contenu
    const routes = {
      "/": () => creer("h1", "Bienvenue au centre de formation"),
      "/formations": () => {
        const ul = document.createElement("ul");
        formations.forEach((nom, i) => {
          const li = document.createElement("li");
          const lien = creer("a", nom);
          lien.href = `/formations/${i + 1}`;
          lien.dataset.lien = "";
          li.append(lien);
          ul.append(li);
        });
        return [creer("h1", "Nos formations"), ul];
      },
      "/contact": () => creer("h1", "Contact : contact@centre.tn"),
    };
    
    function creer(balise, texte) {
      const element = document.createElement(balise);
      element.textContent = texte;
      return element;
    }
    
    function pageFormation(numero) {
      const nom = formations[numero - 1];
      return nom ? [creer("h1", nom), creer("p", `Fiche de la formation n° ${numero}.`)] : null;
    }
    
    function afficher(chemin) {
      const detail = chemin.match(/^\/formations\/(\d+)$/);
      const contenu = detail ? pageFormation(Number(detail[1])) : routes[chemin]?.();
      app.replaceChildren(...[contenu ?? creer("h1", "404 – Page introuvable")].flat());
      document.title = `${app.querySelector("h1").textContent} – Centre`;
    }
    
    function naviguer(chemin) {
      if (chemin !== location.pathname) {
        history.pushState({ chemin }, "", chemin);
      }
      afficher(chemin);
    }
    
    // Délégation : un seul écouteur pour tous les liens marqués data-lien
    document.addEventListener("click", (event) => {
      const lien = event.target.closest("a[data-lien]");
      if (lien) {
        event.preventDefault();
        naviguer(lien.getAttribute("href"));
      }
    });
    
    // Boutons Précédent / Suivant
    window.addEventListener("popstate", () => afficher(location.pathname));
    
    // Premier affichage (y compris si l'utilisateur arrive directement sur /contact)
    afficher(location.pathname);
  • Table des routes: chaque adresse est associée à une fonction qui construit le contenu (avec createElement et textContent, sans innerHTML).
  • Route avec paramètre: /formations/2 est reconnue par l'expression régulière /^\/formations\/(\d+)$/, qui capture le numéro.
  • Délégation: un seul écouteur sur document intercepte tous les liens marqués data-lien, même ceux créés plus tard (liens de la liste des formations). Les autres liens (sites externes) gardent leur comportement normal.
  • popstate: aux boutons Précédent / Suivant, on réaffiche simplement la page correspondant à location.pathname.
  • Premier affichage: afficher(location.pathname) au chargement permet d'arriver directement sur /contact depuis un favori.
  • Le titre de l'onglet est mis à jour à chaque page, ce qui aide l'historique, les favoris et les lecteurs d'écran.
  • Le serveur : renvoyer toujours index.html

    • Testez l'exemple, allez sur « Contact », puis appuyez sur F5. Avec un serveur ordinaire (Live Server), vous obtenez une erreur 404: le navigateur demande le fichier /contact, qui n'existe pas sur le disque.
    • Le serveur d'une SPA doit donc renvoyer index.html pour toutes les adresses de l'application (règle de « repli » ou fallback), et laisser le routeur JavaScript choisir le contenu. C'est aussi pourquoi le script est chargé avec un chemin absolu (/routeur.js).
    • Pour tester, ce petit serveur Python suffit (python serveur.py, puis http://127.0.0.1:8000) :
    # Petit serveur de test : renvoie index.html pour toute adresse sans extension
    import http.server, os
    class Spa(http.server.SimpleHTTPRequestHandler):
        def send_head(self):
            if "." not in os.path.basename(self.path.split("?")[0]):
                self.path = "/index.html"
            return super().send_head()
    http.server.ThreadingHTTPServer(("127.0.0.1", 8000), Spa).serve_forever()
    • En production, une seule ligne de configuration :
    # Nginx
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    # Apache (fichier .htaccess)
    FallbackResource /index.html
    • Vite (chapitre 24) applique déjà cette règle pendant le développement. Sur GitHub Pages, qui ne permet pas cette configuration, on copie index.html en 404.html, ou on utilise des adresses avec #.
  • L'alternative : le routage par hash (#)

    • Avec des adresses comme index.html#/contact, la partie après # n'est jamais envoyée au serveur : aucune configuration n'est nécessaire et cela fonctionne même avec Live Server.
    • On lit location.hash et on écoute l'événement hashchange au lieu de popstate; les liens s'écrivent simplement <a href="#/contact">, sans pushState().
    • Inconvénient : des adresses moins propres et moins bien traitées par les moteurs de recherche. Angular et Vue proposent les deux modes.
  • Nouveauté : la Navigation API

    • L'API History a des défauts connus : pushState() ne déclenche aucun événement, il faut intercepter soi-même chaque lien et chaque formulaire, et popstate ne couvre que Précédent / Suivant.
    • La Navigation API (objet navigation) la remplace : un événement navigate unique est déclenché pour toute navigation (clic sur un lien, formulaire, Précédent / Suivant, navigation.navigate()), et event.intercept() permet de la traiter en JavaScript.
    • Disponible dans Chrome et Edge depuis 2022, elle est arrivée dans Safari 26.2 (décembre 2025) puis Firefox 147 (janvier 2026) : elle est Baseline depuis le 13 janvier 2026. Le même routeur s'écrit alors :
    // Même routeur avec la Navigation API (Baseline depuis janvier 2026)
    navigation.addEventListener("navigate", (event) => {
      const url = new URL(event.destination.url);
      if (!event.canIntercept || url.origin !== location.origin) {
        return; // lien externe, téléchargement… : comportement normal
      }
      event.intercept({
        handler() {
          afficher(url.pathname);
        },
      });
    });
    • Plus besoin de data-lien, de preventDefault() ni de pushState(): l'adresse est mise à jour par le navigateur. Comme elle est récente, gardez une version avec history pour les navigateurs plus anciens (if ("navigation" in window)). L'API History reste à connaître : elle est partout, et encore utilisée par les frameworks.
  • Activités

  • Activité 1 : tester le routeur

    • Dans le dossier spa de votre dépôt js-api-navigateur, reproduisez l'exemple et lancez python serveur.py.
    • Naviguez entre les pages et ouvrez la console : tapez history.length après chaque clic. Utilisez Précédent et Suivant.
    • Lancez la même page avec Live Server, allez sur « Contact » et appuyez sur F5. Expliquez l'erreur.

    Activité 2 : compléter le routeur

    • Ajoutez une page /a-propos et un lien dans le menu.
    • Ajoutez la classe active au lien du menu correspondant à la page affichée.
    • Remplacez la liste formations par un fichier formations.json chargé avec fetch, en affichant « Chargement… » pendant la requête.

    Activité 3 : filtre dans l'adresse

    • Sur la page des formations, ajoutez un champ de recherche. À chaque saisie, filtrez la liste et mettez à jour l'adresse en /formations?q=web avec replaceState().
    • Vérifiez qu'en ouvrant directement cette adresse, le champ est prérempli et la liste filtrée (URLSearchParams). Pourquoi utilise-t-on replaceState() plutôt que pushState() ici ?

    Activité 4 : version hash

    • Adaptez le routeur au routage par hash (#/formations), puis vérifiez qu'il fonctionne avec Live Server, y compris après F5.
    Corrigés. Activité 1 : history.length augmente de 1 à chaque pushState(); avec Live Server, F5 demande au serveur le fichier /contact, qui n'existe pas, d'où la 404 : il manque la règle de repli vers index.html. Activité 3 : avec pushState(), chaque lettre tapée créerait une entrée dans l'historique et le bouton Précédent deviendrait inutilisable ; replaceState() met l'adresse à jour sans ajouter d'entrée. Activité 4 : afficher(location.hash.slice(1) || "/"), appelée au chargement et sur l'événement hashchange.
  • À retenir

    • Une SPA charge une seule page et remplace son contenu en JavaScript.
    • history.pushState() change l'adresse sans recharger ; popstate gère Précédent / Suivant ; replaceState() met à jour sans nouvelle entrée.
    • Un routeur associe chaque adresse à une fonction d'affichage ; le serveur doit renvoyer index.html pour toutes les adresses.
    • La Navigation API (Baseline depuis janvier 2026) simplifie le routage avec un seul événement navigate.

    Sources : MDN Web Docs, web.dev, W3C, WHATWG, open-meteo.com.