</> HTML5Advent
ENFRESDEITPT

// apis · Web Platform Advent #23

IndexedDB, c'est quoi ? La base de données du navigateur expliquée

IndexedDB est la vraie base de données du navigateur : asynchrone, transactionnelle et bien plus vaste que localStorage. Comment fonctionne onupgradeneeded, pourquoi les transactions se referment, à quoi servent les index, et quand la préférer au Web Storage.

Un mur de tiroirs de fichier en bois avec porte-étiquettes en laiton et numéros manuscrits

IndexedDB est la vraie base de données du navigateur. Là où le Web Storage vous donne une boîte synchrone de chaînes plafonnée autour de 5 Mo, IndexedDB offre un magasin asynchrone et transactionnel pour des données structurées, dimensionné en centaines de mégaoctets ou davantage. Ce n'est pas du SQL. C'est un magasin clé-valeur doté d'index, et une fois cela compris le reste de l'API suit.

Ouvrir une base, et le seul endroit où l'on peut changer sa forme

On ouvre une base par son nom et un numéro de version. Si la version demandée est supérieure à celle présente sur le disque, le navigateur déclenche upgradeneeded, et ce gestionnaire est le seul endroit où l'on peut créer ou supprimer des magasins d'objets et des index.

const request = indexedDB.open('notes-db', 1);

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

request.onsuccess = (event) => {
  const db = event.target.result;
};

request.onerror = () => console.error(request.error);

Cela surprend parce que cela inverse l'habitude. Impossible de créer un magasin paresseusement la première fois qu'on en a besoin. Le schéma vit dans une seule fonction, liée à un numéro de version que l'on incrémente quand la structure change.

Tout est une requête, et les requêtes sont asynchrones

Presque chaque appel IndexedDB renvoie un IDBRequest plutôt qu'une valeur. On lit le résultat dans onsuccess et on traite l'échec dans onerror. Rien ne bloque le thread principal, ce qui est toute la raison de la préférer à localStorage dès que le volume compte.

Un tiroir ouvert de fiches cartonnées avec des intercalaires numérotés de 166 à 169. Un index IndexedDB est exactement cela : un second classement des mêmes enregistrements, tenu pour pouvoir les retrouver par autre chose que leur clé.
Un tiroir ouvert de fiches cartonnées avec des intercalaires numérotés de 166 à 169. Un index IndexedDB est exactement cela : un second classement des mêmes enregistrements, tenu pour pouvoir les retrouver par autre chose que leur clé.

Lectures et écritures se font dans une transaction

On ne touche pas un magasin directement. On ouvre une transaction sur un ou plusieurs magasins, en mode readonly ou readwrite, et l'on travaille à travers elle.

const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');

store.add({ title: 'Buy milk', tag: 'errand', done: false });

tx.oncomplete = () => console.log('written');
tx.onerror = () => console.error(tx.error);

Voici le piège qu'il vaut mieux connaître à l'avance. Une transaction valide automatiquement dès qu'il n'y a plus de travail en attente pour elle. Si vous attendez quelque chose d'étranger au milieu, un fetch par exemple, la transaction se referme pendant l'attente et l'appel suivant lève TransactionInactiveError. Faites d'abord le travail extérieur, puis ouvrez la transaction.

Les index, qui justifient le nom

Par défaut, les enregistrements se récupèrent par leur clé primaire. Un index est un second classement des mêmes enregistrements, qui permet d'interroger par une autre propriété.

const tx = db.transaction('notes', 'readonly');
const index = tx.objectStore('notes').index('by_tag');

const req = index.getAll('errand');
req.onsuccess = () => console.log(req.result);

Pour des plages plutôt que des correspondances exactes, combinez un index avec IDBKeyRange et un curseur, qui parcourt les résultats un par un au lieu de tous les charger en mémoire.

Ce qu'elle peut stocker, et combien

IndexedDB accepte tout ce que gère l'algorithme de clonage structuré : objets, tableaux, Date, Blob, File, ArrayBuffer. Les fonctions et les nœuds du DOM ne sont pas clonables et lèvent une erreur. Aucun aller-retour par JSON.stringify n'est nécessaire, ce qui signifie aussi que les nombres restent des nombres et les dates des dates.

Le quota est une part de l'espace disque disponible et non un plafond fixe de 5 Mo : c'est donc le bon foyer pour des réponses d'API en cache, des documents hors ligne ou des médias. Ce stockage est au mieux par défaut et peut être évincé sous pression disque ; appelez navigator.storage.persist() si les données doivent y survivre, et navigator.storage.estimate() pour voir ce que vous consommez.

IndexedDB ou localStorage ?

localStorageIndexedDB
Style d'APISynchrone, bloque le thread principalAsynchrone, à base d'événements
ValeursChaînes uniquementValeurs clonables structurées
Taille indicativeEnviron 5 Mo par origineUne part de l'espace disque libre
InterrogationPar clé seulementPar clé, par index, par plage avec curseurs
Adapté àThème, drapeaux, petites préférencesDonnées hors ligne, caches, fichiers, longues listes

Les deux sont limités à l'origine et aucun n'est un endroit pour des secrets : n'importe quel script de la page peut les lire.

Faut-il passer par une bibliothèque ?

L'API brute est verbeuse parce qu'elle est antérieure aux promesses. De petites bibliothèques enveloppent les requêtes dans des promesses et suppriment l'essentiel du code répétitif ; c'est un choix par défaut raisonnable pour du code applicatif. Apprenez tout de même le modèle sous-jacent : upgradeneeded, les transactions et leur validation automatique se comportent pareil en dessous, et tous les bugs déroutants que vous rencontrerez viennent de ces trois points, pas de la bibliothèque.

Choisissez IndexedDB quand les données sont structurées, volumineuses, ou doivent être disponibles hors ligne, souvent aux côtés d'un service worker. Gardez localStorage pour la poignée de petits drapeaux où une lecture synchrone est réellement plus simple.