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

Introduction à IndexedDB

Découvrez IndexedDB, la base de données intégrée au navigateur : quand la préférer au localStorage, vocabulaire (base, version, magasin d'objets, clé, index, transaction, requête), création de la base...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Découvrez IndexedDB, la base de données intégrée au navigateur : quand la préférer au localStorage, vocabulaire (base, version, magasin d'objets, clé, index, transaction, requête), création de la base avec onupgradeneeded, module complet avec promesses pour ajouter, lire, chercher par index et supprimer, exemple du carnet des stagiaires, curseurs, espace disponible et stockage persistant, bibliothèques idb et Dexie. Avec des activités corrigées.
  1. Objectifs

    • Savoir quand utiliser IndexedDB plutôt que le localStorage.
    • Comprendre son vocabulaire : base, version, magasin d'objets, clé, index, transaction, requête.
    • Créer une base, ajouter, lire, chercher et supprimer des enregistrements.
    • Utiliser IndexedDB avec des promesses et async/await.
  2. Pourquoi IndexedDB ?

    • Le localStorage est parfait pour quelques préférences, mais il montre vite ses limites : environ 5 Mo, uniquement du texte, aucune recherche (il faut tout relire et filtrer en JavaScript), et des appels synchrones qui bloquent la page.
    • IndexedDB est une vraie base de données intégrée au navigateur :
    CaractéristiqueIndexedDB
    CapacitéTrès grande : une part importante de l'espace disque libre (des centaines de Mo, souvent plus)
    DonnéesObjets JavaScript directement : nombres, dates, tableaux, objets imbriqués, fichiers, images (Blob)
    RecherchePar clé et par index (comme les index d'une base SQL), avec des intervalles
    FonctionnementAsynchrone: ne bloque jamais la page
    FiabilitéTransactions: un ensemble d'opérations réussit entièrement ou est annulé
    DisponibilitéPage, Web Workers et Service Workers (mode hors ligne)
    • Exemples d'usage : application qui fonctionne hors ligne (PWA), messagerie qui garde les messages, éditeur de documents, catalogue de produits, fichiers ou photos mis en cache.
    • Ce n'est pas une base SQL : pas de tables ni de requêtes SELECT, mais des magasins d'objets (object stores), proches des collections de MongoDB.
  3. Le vocabulaire

  4. Structure d'une base IndexedDB : magasin d'objets, clés, index et étapes d'utilisation

  • Base de données: identifiée par un nom et un numéro de version (entier). Un site peut en avoir plusieurs.
  • Magasin d'objets (object store) : l'équivalent d'une table. Il contient des enregistrements clé → objet.
  • Clé: identifiant unique de chaque enregistrement. Avec keyPath: "id", la clé est la propriété id de l'objet ; avec autoIncrement: true, elle est générée automatiquement (1, 2, 3…).
  • Index: permet de rechercher rapidement sur une autre propriété (par exemple tous les stagiaires du groupe DEV-101).
  • Transaction: toute lecture ou écriture se fait dans une transaction, en mode "readonly" ou "readwrite".
  • Requête: chaque opération renvoie un objet IDBRequest; le résultat arrive plus tard, dans l'événement success (ou error).
  • Ouvrir et créer la base

  • const requete = indexedDB.open("centre-formation", 1);
    
    requete.onupgradeneeded = () => {
      // Seul endroit où l'on peut créer ou modifier la structure
      const db = requete.result;
      const magasin = db.createObjectStore("stagiaires", { keyPath: "id", autoIncrement: true });
      magasin.createIndex("parGroupe", "groupe", { unique: false });
    };
    
    requete.onsuccess = () => {
      const db = requete.result; // la base est prête
    };
    
    requete.onerror = () => console.error(requete.error);
    • L'événement upgradeneeded se déclenche quand la base n'existe pas encore, ou quand on l'ouvre avec un numéro de version plus grand. Pour ajouter un magasin ou un index plus tard, on passe à la version 2 et on complète onupgradeneeded (en testant event.oldVersion).
    • On ne peut pas créer de magasin ailleurs que dans onupgradeneeded.
    1. Des promesses pour simplifier

      • L'API d'IndexedDB date d'avant les promesses : elle utilise des événements onsuccess / onerror, ce qui rend le code vite difficile à lire. Une petite fonction enPromesse() permet d'utiliser await, comme pour fetch.
      • Voici un module complet, db.js, pour gérer un carnet de stagiaires :
      // Accès à IndexedDB, enveloppé dans des promesses
      
      const NOM_BASE = "centre-formation";
      const VERSION = 1;
      
      // Transforme une requête IndexedDB (onsuccess / onerror) en promesse
      function enPromesse(requete) {
        return new Promise((resolve, reject) => {
          requete.onsuccess = () => resolve(requete.result);
          requete.onerror = () => reject(requete.error);
        });
      }
      
      export function ouvrirBase() {
        const requete = indexedDB.open(NOM_BASE, VERSION);
      
        // Appelé à la création de la base, puis à chaque changement de VERSION
        requete.onupgradeneeded = () => {
          const db = requete.result;
          if (!db.objectStoreNames.contains("stagiaires")) {
            const magasin = db.createObjectStore("stagiaires", { keyPath: "id", autoIncrement: true });
            magasin.createIndex("parGroupe", "groupe", { unique: false });
          }
        };
        return enPromesse(requete);
      }
      
      function magasin(db, mode = "readonly") {
        return db.transaction("stagiaires", mode).objectStore("stagiaires");
      }
      
      export function ajouter(db, stagiaire) {
        return enPromesse(magasin(db, "readwrite").add(stagiaire)); // renvoie l'id créé
      }
      
      export function lister(db) {
        return enPromesse(magasin(db).getAll());
      }
      
      export function listerParGroupe(db, groupe) {
        return enPromesse(magasin(db).index("parGroupe").getAll(groupe));
      }
      
      export function supprimer(db, id) {
        return enPromesse(magasin(db, "readwrite").delete(id));
      }
      Méthode du magasinEffet
      add(objet)Ajoute ; erreur si la clé existe déjà
      put(objet)Ajoute ou remplace (pour modifier un enregistrement)
      get(cle)Lit un enregistrement (undefined s'il n'existe pas)
      getAll(), getAll(intervalle)Lit tous les enregistrements, ou ceux d'un intervalle de clés
      delete(cle), clear()Supprime un enregistrement, ou tout le magasin
      count()Nombre d'enregistrements
      index("parGroupe").getAll("DEV-101")Recherche par index
    2. Exemple complet : le carnet des stagiaires

    3. index.html:

      <!doctype html>
      <html lang="fr">
        <head>
          <meta charset="utf-8" />
          <title>Carnet des stagiaires (IndexedDB)</title>
          <script type="module" src="app.js"></script>
        </head>
        <body>
          <h1>Carnet des stagiaires</h1>
          <form id="form-stagiaire">
            <input id="nom" placeholder="Nom et prénom" required />
            <select id="groupe">
              <option>DEV-101</option>
              <option>DEV-102</option>
            </select>
            <input id="note" type="number" min="0" max="20" step="0.25" placeholder="Note" required />
            <button type="submit">Ajouter</button>
          </form>
      
          <p>
            Afficher :
            <select id="filtre">
              <option value="">Tous les groupes</option>
              <option>DEV-101</option>
              <option>DEV-102</option>
            </select>
          </p>
          <ul id="liste"></ul>
        </body>
      </html>

      app.js (module, d'où le await au premier niveau) :

      import { ouvrirBase, ajouter, lister, listerParGroupe, supprimer } from "./db.js";
      
      const db = await ouvrirBase();
      const liste = document.getElementById("liste");
      const filtre = document.getElementById("filtre");
      
      async function afficher() {
        const stagiaires = filtre.value ? await listerParGroupe(db, filtre.value) : await lister(db);
        liste.replaceChildren(
          ...stagiaires.map((stagiaire) => {
            const li = document.createElement("li");
            li.textContent = `${stagiaire.nom} (${stagiaire.groupe}) : ${stagiaire.note}/20 `;
            const bouton = document.createElement("button");
            bouton.textContent = "Supprimer";
            bouton.addEventListener("click", async () => {
              await supprimer(db, stagiaire.id);
              afficher();
            });
            li.append(bouton);
            return li;
          }),
        );
      }
      
      document.getElementById("form-stagiaire").addEventListener("submit", async (event) => {
        event.preventDefault();
        await ajouter(db, {
          nom: document.getElementById("nom").value.trim(),
          groupe: document.getElementById("groupe").value,
          note: Number(document.getElementById("note").value),
          ajouteLe: new Date(), // IndexedDB stocke les dates telles quelles (pas besoin de JSON)
        });
        event.target.reset();
        afficher();
      });
      
      filtre.addEventListener("change", afficher);
      afficher();
    • Les objets sont enregistrés tels quels, sans JSON.stringify(): la date ajouteLe est relue comme un vrai objet Date.
    • Le filtre par groupe utilise l'index parGroupe: le navigateur ne parcourt pas toute la base.
    • Pour inspecter la base : DevTools → Application → Storage → IndexedDB → centre-formation → stagiaires.
  • Parcourir avec un curseur

    • Pour traiter les enregistrements un par un (gros volumes, ordre inverse, arrêt en cours de route), on ouvre un curseur:
    // Parcourir les stagiaires un par un avec un curseur, du plus récent au plus ancien
    const transaction = db.transaction("stagiaires");
    const requete = transaction.objectStore("stagiaires").openCursor(null, "prev");
    
    requete.onsuccess = () => {
      const curseur = requete.result;
      if (curseur) {
        console.log(curseur.key, curseur.value.nom);
        curseur.continue(); // passe à l'enregistrement suivant
      } else {
        console.log("Fin du parcours");
      }
    };
    • "prev" parcourt de la plus grande clé à la plus petite ; IDBKeyRange.bound(10, 20) limite le parcours à un intervalle de clés.
  • Espace disponible et persistance

  • // Combien d'espace le site utilise-t-il, et combien lui en reste-t-il ?
    const { usage, quota } = await navigator.storage.estimate();
    console.log(
      `Utilisé : ${(usage / 1024 / 1024).toFixed(1)} Mo sur ${(quota / 1024 / 1024).toFixed(0)} Mo`,
    );
    
    // Demander que les données ne soient pas effacées automatiquement si le disque se remplit
    const persistant = await navigator.storage.persist();
    console.log(persistant ? "Stockage persistant accordé" : "Stockage « au mieux » (best-effort)");
    • Par défaut, le stockage est « au mieux » (best-effort) : si le disque est presque plein, le navigateur peut effacer les données des sites peu visités. navigator.storage.persist() demande à les protéger (Firefox pose la question à l'utilisateur, Chrome décide selon l'usage du site).
  • Aller plus loin

    • En entreprise, on utilise rarement l'API brute : la petite bibliothèque idb (de Jake Archibald, environ 1 Ko) fournit directement des promesses (await db.getAll("stagiaires")), et Dexie.js offre des requêtes plus riches et la synchronisation avec un serveur.
    • IndexedDB est la base des applications web hors ligne: un Service Worker met en cache les fichiers du site, et IndexedDB les données de l'utilisateur.
    • Comme le localStorage, IndexedDB n'est pas chiffré : pas de données sensibles sans précaution.
  • Activités

  • Activité 1 : le carnet des stagiaires

    • Dans le dossier indexeddb de votre dépôt js-stockage-navigateur, reproduisez l'exemple (avec Live Server). Ajoutez 5 stagiaires et observez la base dans DevTools.
    • Ajoutez l'affichage de la moyenne du groupe sélectionné et le nombre de stagiaires (count()).

    Activité 2 : modifier un stagiaire

    • Ajoutez un bouton « Modifier la note » : il demande la nouvelle note, lit le stagiaire avec get(), change sa note et l'enregistre avec put().

    Activité 3 : nouvelle version de la base

    • Passez la base en version 2 et ajoutez, dans onupgradeneeded, un index parNote sur la propriété note, sans perdre les données existantes.
    • Affichez les stagiaires ayant au moins 10 avec index("parNote").getAll(IDBKeyRange.lowerBound(10)).

    Activité 4 : questions

    • Citez trois différences entre IndexedDB et le localStorage.
    • Dans quel événement crée-t-on les magasins d'objets ? Quand est-il déclenché ?
    • Quelle est la différence entre add() et put()?
    Corrigés. Activité 3 : dans onupgradeneeded, on récupère le magasin existant avec requete.transaction.objectStore("stagiaires") et on appelle createIndex("parNote", "note") seulement si event.oldVersion < 2; les données sont conservées. Activité 4 : IndexedDB est asynchrone, stocke des objets (pas seulement du texte), offre des index et des transactions et une capacité bien plus grande ; dans onupgradeneeded, déclenché à la création de la base ou à l'ouverture avec une version plus grande ; add() échoue si la clé existe déjà, put() remplace l'enregistrement existant.
  • À retenir

    • IndexedDB : une base de données asynchrone, pour de gros volumes d'objets, avec index et transactions.
    • indexedDB.open(nom, version); la structure se crée dans onupgradeneeded.
    • db.transaction(magasin, mode).objectStore(magasin) puis add, put, get, getAll, delete, index().
    • On enveloppe les requêtes dans des promesses (ou on utilise la bibliothèque idb) pour écrire avec await.

    Sources : MDN Web Docs, web.dev, CNIL, INPDP.