</> HTML5Advent
ENFRESDEITPT

// apis · Web Platform Advent #23

IndexedDB cos'è: il database del browser spiegato

IndexedDB è il vero database del browser: asincrono, transazionale e molto più capiente di localStorage. Come funziona onupgradeneeded, perché le transazioni si chiudono da sole, a cosa servono gli indici e quando usarlo al posto del Web Storage.

Una parete di cassetti di schedario in legno con portaetichette in ottone e numeri scritti a mano

IndexedDB è il vero database del browser. Dove il Web Storage ti dà una scatola sincrona di stringhe con un tetto attorno ai 5 MB, IndexedDB offre un archivio asincrono e transazionale per dati strutturati, dimensionato in centinaia di megabyte o più. Non è SQL. È un archivio chiave-valore con indici, e una volta afferrato questo il resto dell'API viene da sé.

Aprire un database, e l'unico posto dove puoi cambiarne la forma

Un database si apre per nome e numero di versione. Se la versione che chiedi è più alta di quella su disco, il browser scatena upgradeneeded, e quel gestore è l'unico punto in cui puoi creare o eliminare object store e indici.

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);

Questo spiazza perché ribalta l'abitudine. Non puoi creare uno store pigramente la prima volta che ti serve. Lo schema vive in una sola funzione, legata a un numero di versione che alzi quando la struttura cambia.

Tutto è una richiesta, e le richieste sono asincrone

Quasi ogni chiamata IndexedDB restituisce un IDBRequest invece di un valore. Il risultato si legge in onsuccess e l'errore si gestisce in onerror. Niente blocca il thread principale, ed è tutta la ragione per preferirlo a localStorage quando il volume conta.

Un cassetto aperto di schede con separatori numerati dal 166 al 169. Un indice di IndexedDB è esattamente questo: un secondo ordinamento degli stessi record, tenuto per poterli cercare con qualcosa di diverso dalla loro chiave.
Un cassetto aperto di schede con separatori numerati dal 166 al 169. Un indice di IndexedDB è esattamente questo: un secondo ordinamento degli stessi record, tenuto per poterli cercare con qualcosa di diverso dalla loro chiave.

Letture e scritture avvengono dentro una transazione

Uno store non si tocca direttamente. Si apre una transazione su uno o più store, in modalità readonly o readwrite, e ci si lavora attraverso.

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);

Ecco la trappola che conviene conoscere in anticipo. Una transazione viene confermata automaticamente appena non ha più lavoro in sospeso. Se nel mezzo attendi qualcosa di estraneo, per esempio un fetch, la transazione si chiude durante l'attesa e la chiamata successiva solleva TransactionInactiveError. Fai prima il lavoro esterno, poi apri la transazione.

Gli indici, che spiegano il nome

Per impostazione predefinita i record si recuperano dalla chiave primaria. Un indice è un secondo ordinamento degli stessi record, che consente di interrogare per un'altra proprietà.

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);

Per intervalli anziché corrispondenze esatte, combina un indice con IDBKeyRange e un cursore, che scorre i risultati uno alla volta invece di caricarli tutti in memoria.

Che cosa può conservare, e quanto

IndexedDB accetta tutto ciò che l'algoritmo di clonazione strutturata gestisce: oggetti, array, Date, Blob, File, ArrayBuffer. Funzioni e nodi del DOM non sono clonabili e sollevano un errore. Non serve alcun passaggio da JSON.stringify, il che significa anche che i numeri restano numeri e le date restano date.

La quota è una quota del disco disponibile e non un tetto fisso di 5 MB, quindi è il posto giusto per risposte API in cache, documenti offline o file multimediali. Quell'archiviazione è best effort per impostazione predefinita e può essere sfrattata sotto pressione di spazio; chiama navigator.storage.persist() se i dati devono sopravvivere, e navigator.storage.estimate() per vedere quanto stai usando.

IndexedDB o localStorage?

localStorageIndexedDB
Stile di APISincrona, blocca il thread principaleAsincrona, basata su eventi
ValoriSolo stringheValori clonabili strutturati
Dimensione indicativaCirca 5 MB per origineUna quota dello spazio libero su disco
InterrogazioniSolo per chiavePer chiave, per indice, per intervallo con cursori
Adatto aTema, flag, piccole preferenzeDati offline, cache, file, liste grandi

Entrambi sono limitati all'origine e nessuno dei due è un posto per i segreti: qualsiasi script della pagina può leggerli.

Conviene usare un wrapper?

L'API grezza è verbosa perché precede le promise. Piccole librerie avvolgono le richieste in promise e tolgono quasi tutto il codice ripetitivo, e sono una scelta predefinita ragionevole per il codice applicativo. Impara comunque prima il modello sottostante: upgradeneeded, le transazioni e la loro conferma automatica si comportano allo stesso modo sotto, e ogni bug sconcertante che incontrerai viene da quei tre punti, non dal wrapper.

Scegli IndexedDB quando i dati sono strutturati, voluminosi o devono essere disponibili offline, spesso accanto a un service worker. Tieni localStorage per la manciata di piccoli flag dove una lettura sincrona è davvero più semplice.