</> HTML5Advent
ENFRESDEITPT

// apis

Tutoriel IndexedDB : construire une application de notes qui fonctionne hors ligne

Un tutoriel IndexedDB pas a pas. Ouvrir une base de donnees, creer un object store, ajouter et lire des enregistrements, interroger par index, supprimer des entrees et gerer les montees de version, le tout dans une petite application de notes executable dans le navigateur.

Une personne debout devant des baies de serveurs vitrees dans un datacenter, tenant un ordinateur portable ouvert, avec un eclairage bleu et des logos de fournisseurs visibles sur les panneaux vitres

Le guide conceptuel explique ce qu'est IndexedDB et quand l'utiliser. Ce tutoriel construit une petite application de notes en partant de zero pour que vous voyiez chaque etape : ouvrir une base de donnees, creer un store, ecrire des enregistrements, les relire, interroger par index et supprimer des entrees.

Tout s'execute dans le navigateur. Pas d'outils de build, pas de serveur, pas de dependances. Copiez chaque bloc dans un seul fichier HTML et ouvrez-le.

Etape 1 : ouvrir la base de donnees

Toute interaction avec IndexedDB commence par indexedDB.open(). Vous passez un nom et un numero de version. Si la base n'existe pas encore, ou si votre version est superieure a celle sur le disque, le navigateur declenche upgradeneeded.

function openDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('notes-app', 1);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains('notes')) {
        const store = db.createObjectStore('notes', {
          keyPath: 'id',
          autoIncrement: true,
        });
        store.createIndex('by_tag', 'tag', { unique: false });
        store.createIndex('by_date', 'createdAt', { unique: false });
      }
    };

    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Quelques points a remarquer. keyPath: 'id' indique a IndexedDB quelle propriete de chaque objet est la cle primaire. autoIncrement: true fait generer cette cle par la base de donnees. Les deux appels createIndex vous permettront d'interroger les notes par tag ou par date sans parcourir tous les enregistrements.

Etape 2 : ajouter un enregistrement

Ecrire dans IndexedDB se fait toujours dans une transaction. Vous en ouvrez une sur l'object store dont vous avez besoin, obtenez une reference a ce store, et appelez add() ou put().

async function addNote(db, text, tag) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const record = {
      text,
      tag: tag || 'general',
      createdAt: new Date().toISOString(),
    };
    const request = store.add(record);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

add() leve une erreur si un enregistrement avec la meme cle existe deja. put() ecrase a la place. Pour les nouveaux enregistrements, add() est plus sur car il attrape les doublons accidentels.

Etape 3 : lire tous les enregistrements

Pour tout lire dans un store, ouvrez une transaction readonly et appelez getAll() :

async function getAllNotes(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const request = store.getAll();
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Si vous n'avez besoin que d'un enregistrement et que vous connaissez sa cle, utilisez store.get(key) a la place. Il renvoie un seul objet plutot qu'un tableau.

Gros plan d'un editeur de code a theme sombre sur un ecran, montrant du code de configuration Ruby avec coloration syntaxique verte et blanche, une arborescence a gauche et des numeros de ligne a droite
Ecrire du code dans un editeur sombre. IndexedDB suit le meme schema requete-et-callback que la plupart des API du navigateur, enveloppe ici dans des promesses pour la lisibilite.

Etape 4 : interroger par index

Les index permettent de trouver des enregistrements sans parcourir tout le store. Pour obtenir toutes les notes taguees "work" :

async function getNotesByTag(db, tag) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readonly');
    const store = tx.objectStore('notes');
    const index = store.index('by_tag');
    const request = index.getAll(tag);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

Vous pouvez aussi utiliser un key range pour interroger une plage de valeurs. Par exemple, pour obtenir les notes creees apres une certaine date :

const range = IDBKeyRange.lowerBound('2026-01-01T00:00:00.000Z');
const request = store.index('by_date').getAll(range);

Les quatre constructeurs de range sont lowerBound, upperBound, bound (les deux extremites) et only (correspondance exacte). Chacun accepte un booleen optionnel pour exclure la valeur limite.

Etape 5 : mettre a jour un enregistrement

La mise a jour utilise put(). Vous devez inclure la propriete cle (id dans notre schema) pour qu'IndexedDB sache quel enregistrement ecraser :

async function updateNote(db, id, newText) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const getReq = store.get(id);
    getReq.onsuccess = () => {
      const note = getReq.result;
      if (!note) { reject(new Error('Not found')); return; }
      note.text = newText;
      const putReq = store.put(note);
      putReq.onsuccess = () => resolve();
      putReq.onerror = () => reject(putReq.error);
    };
    getReq.onerror = () => reject(getReq.error);
  });
}

Etape 6 : supprimer un enregistrement

Appelez store.delete(key) dans une transaction readwrite :

async function deleteNote(db, id) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('notes', 'readwrite');
    const store = tx.objectStore('notes');
    const request = store.delete(id);
    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
}

Pour vider tous les enregistrements d'un store, utilisez store.clear() a la place.

Etape 7 : gerer les montees de version

Quand vous devez ajouter un nouvel index ou un nouveau store apres que les utilisateurs ont deja la version 1, incrementez le numero de version et gerez la migration dans onupgradeneeded :

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  const oldVersion = event.oldVersion;

  if (oldVersion < 1) {
    const store = db.createObjectStore('notes', {
      keyPath: 'id',
      autoIncrement: true,
    });
    store.createIndex('by_tag', 'tag', { unique: false });
    store.createIndex('by_date', 'createdAt', { unique: false });
  }

  if (oldVersion < 2) {
    const tx = event.target.transaction;
    const store = tx.objectStore('notes');
    store.createIndex('by_priority', 'priority', { unique: false });
  }
};

Verifiez event.oldVersion pour n'executer que les migrations que l'utilisateur n'a pas encore vues. Un utilisateur qui passe de 0 (pas de base) execute les deux blocs. Un utilisateur qui passe de 1 n'execute que le second.

Assembler le tout

Voici un script minimal qui exerce toutes les fonctions ci-dessus :

(async () => {
  const db = await openDB();

  const id = await addNote(db, 'Buy groceries', 'personal');
  console.log('Added note with id:', id);

  const all = await getAllNotes(db);
  console.log('All notes:', all);

  const tagged = await getNotesByTag(db, 'personal');
  console.log('Personal notes:', tagged);

  await updateNote(db, id, 'Buy groceries and cook dinner');
  console.log('Updated note', id);

  await deleteNote(db, id);
  console.log('Deleted note', id);
})();

Ouvrez la console du navigateur pour voir la sortie. Chaque operation est asynchrone, chaque ecriture passe par une transaction, et les donnees survivent aux rechargements de page sans aucun serveur.

Erreurs courantes

ErreurCe qui se passeCorrection
Creer un store en dehors de onupgradeneededInvalidStateErrorTous les changements de schema vont dans onupgradeneeded
Utiliser await dans un callback de transactionLa transaction se ferme automatiquement avant que l'await se resolveGardez toutes les operations sur le store synchrones dans un meme tick de transaction, ou ouvrez une nouvelle transaction apres l'await
Oublier d'incrementer le numero de versiononupgradeneeded ne se declenche jamaisIncrementez l'entier de version chaque fois que vous modifiez le schema
Passer une chaine la ou une cle attend un nombreEnregistrement non trouveRespectez le type : si autoIncrement genere des nombres, interrogez avec un nombre

IndexedDB est verbeux, mais chaque element a une raison d'etre : la version controle votre schema, les transactions protegent vos donnees, et les index gardent les lectures rapides. Une fois ces sept etapes devenues routine, vous avez tout ce qu'il faut pour stocker des donnees structurees cote client, que ce soit pour le hors ligne, le cache ou un etat qui survit a la session.