À 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.
-
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.
- Savoir quand utiliser IndexedDB plutôt que le
-
Pourquoi IndexedDB ?
- Le
localStorageest 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éristique IndexedDB Capacité Très grande : une part importante de l'espace disque libre (des centaines de Mo, souvent plus) Données Objets JavaScript directement : nombres, dates, tableaux, objets imbriqués, fichiers, images ( Blob)Recherche Par clé et par index (comme les index d'une base SQL), avec des intervalles Fonctionnement Asynchrone: 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.
- Le
-
Le vocabulaire

- 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éidde l'objet ; avecautoIncrement: 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énementsuccess(ouerror).
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
upgradeneededse 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èteonupgradeneeded(en testantevent.oldVersion). - On ne peut pas créer de magasin ailleurs que dans
onupgradeneeded.
-
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 fonctionenPromesse()permet d'utiliserawait, comme pourfetch. - 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 magasin Effet add(objet)Ajoute ; erreur si la clé existe déjà put(objet)Ajoute ou remplace (pour modifier un enregistrement) get(cle)Lit un enregistrement ( undefineds'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 - L'API d'IndexedDB date d'avant les promesses : elle utilise des événements
-
Exemple complet : le carnet des stagiaires
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 dateajouteLeest relue comme un vrai objetDate. - 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
indexeddbde votre dépôtjs-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 avecput().
Activité 3 : nouvelle version de la base
- Passez la base en version 2 et ajoutez, dans
onupgradeneeded, un indexparNotesur 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()etput()?
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 dansonupgradeneeded.db.transaction(magasin, mode).objectStore(magasin)puisadd,put,get,getAll,delete,index().- On enveloppe les requêtes dans des promesses (ou on utilise la bibliothèque
idb) pour écrire avecawait.
Sources : MDN Web Docs, web.dev, CNIL, INPDP.