// 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.
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.
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?
| localStorage | IndexedDB | |
|---|---|---|
| Stile di API | Sincrona, blocca il thread principale | Asincrona, basata su eventi |
| Valori | Solo stringhe | Valori clonabili strutturati |
| Dimensione indicativa | Circa 5 MB per origine | Una quota dello spazio libero su disco |
| Interrogazioni | Solo per chiave | Per chiave, per indice, per intervallo con cursori |
| Adatto a | Tema, flag, piccole preferenze | Dati 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.