IndexedDB in JavaScript: archiviare dati nel browser

IndexedDB in JavaScript: archiviare dati nel browser

IndexedDB è una delle soluzioni di archiviazione introdotte nei browser nel corso degli anni.

È un database chiave/valore (un database NoSQL) considerato la soluzione definitiva per archiviare dati nel browser.

È un’API asincrona: l’esecuzione del codice non blocca il resto del programma, con un vantaggio notevole per l’esperienza utente. Può archiviare una quantità di dati non definita a priori, anche se oltre una certa soglia viene chiesto all’utente di autorizzare il sito a occupare più spazio.

Esistono altri metodi: i cookie e il Web Storage ( localStorage e sessionStorage ). Lo storage local/session ha lo svantaggio di essere limitato a poco spazio, dato che i browser offrono da 2MB a 10MB per sito.

Anche se tecnicamente puoi creare più database per sito, di solito ne crei uno solo e al suo interno crei più object store.

Un database è privato per dominio: nessun altro sito può accedere a un database IndexedDB che non è il suo.

Ogni object store contiene di norma un insieme di elementi, che possono essere:

  • objects
  • arrays
  • strings
  • numbers
  • dates

Per esempio puoi avere uno store che contiene i post e un altro che contiene i commenti.

Uno store contiene un certo numero di oggetti dotati di una chiave univoca, che rappresenta il modo in cui un oggetto viene identificato.

Puoi modificare questi oggetti tramite transazioni, eseguendo operazioni di inserimento, modifica, eliminazione e iterazione sugli elementi che contengono.

Dall’arrivo delle Promise in ES6, e dal successivo passaggio delle API alle promesse, l’API IndexedDB sembra un po’ vecchia scuola.

Non c’è niente di male, ma in tutti gli esempi che seguono userò la libreria promise IndexedDB di Jake Archibald, un sottile strato sopra l’API IndexedDB che la rende più comoda da usare.

La stessa libreria è usata in tutti gli esempi su IndexedDB del sito Google Developers


Creare un database IndexedDB

Il modo più semplice è usare unpkg, aggiungendolo nell’head della pagina:

javascript
<script type="module">
import { openDB, deleteDB } from 'https://unpkg.com/idb?module'
</script>

Prima di usare l’API IndexedDB verifica sempre che il browser la supporti: anche se è ormai ampiamente disponibile, non sai mai quale browser stia usando l’utente.

javascript
(async () => {
  'use strict'

if (!('indexedDB' in window)) {
    console.warn('IndexedDB not supported')
    return
  }
})()

Come creare un database IndexedDB

Con openDB():

javascript
(async () => {
  //...

const dbName="mydbname"
  const storeName="store1"
  const version = 1
  const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
    }
  })
})()

I primi 2 parametri sono il nome del database e la versione. Il terzo, facoltativo, è un oggetto che contiene una funzione chiamata solo se il numero di versione è superiore a quello del database già installato. Nel corpo di quella funzione puoi aggiornare la struttura del database (store e indici).

Aggiungere dati in uno store

Aggiungere dati alla creazione dello store: inizializzare lo store

Per aggiungere dati si usa il metodo put dell’object store, ma prima dobbiamo inizializzarlo con db.createObjectStore() al momento della creazione.

Con put il valore è il primo argomento e la chiave il secondo. Il motivo è che, se specifichi keyPath quando crei l’object store, non devi indicare il nome della chiave a ogni put(): basta passare il valore.

Questo popola store0 appena lo creiamo:

javascript
(async () => {
  //...
  const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(dbName, version,{
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
      store.put('Hello world!', 'Hello')
    }
  })
})()

Aggiungere dati a store già creato, con le transazioni

Per aggiungere elementi in un secondo momento devi creare una transazione in lettura/scrittura, che garantisce l’integrità del database: se un’operazione fallisce, tutte le operazioni della transazione vengono annullate e lo stato torna a quello iniziale.

Usa quindi un riferimento all’oggetto dbPromise ottenuto chiamando openDB, ed esegui:

javascript
(async () => {
  //...
  const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(/* ... */)

const tx = db.transaction(storeName, 'readwrite')
  const store = await tx.objectStore(storeName)

const val="hey!"
  const key = 'Hello again'
  const value = await store.put(val, key)
  await tx.done
})()

Leggere dati da uno store

Leggere un elemento da un object store: get()

javascript
const key = 'Hello again'
const item = await db.transaction(storeName).objectStore(storeName).get(key)

Leggere tutti gli elementi di un object store: getAll()

Ottenere tutte le chiavi archiviate

javascript
const items = await db.transaction(storeName).objectStore(storeName).getAllKeys()

Ottenere tutti i valori archiviati

javascript
const items = await db.transaction(storeName).objectStore(storeName).getAll()

Eliminare dati da IndexedDB

Eliminare il database, un object store e i dati

Eliminare del tutto un database IndexedDB

javascript
const dbName="mydbname"
await deleteDB(dbName)

Per eliminare dati in un object store

Usiamo una transazione:

javascript
(async () => {
  //...

const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      const store = db.createObjectStore(storeName)
    }
  })

const tx = await db.transaction(storeName, 'readwrite')
  const store = await tx.objectStore(storeName)

const key = 'Hello again'
  await store.delete(key)
  await tx.done
})()

Migrare da una versione precedente del database

Il terzo parametro (facoltativo) della funzione openDB() è un oggetto che può contenere una funzione di aggiornamento, chiamata solo se il numero di versione è superiore a quello del database installato. Nel corpo di quella funzione puoi aggiornare la struttura del database (store e indici):

javascript
const name="mydbname"
const version = 1
openDB(name, version, {
  upgrade(db, oldVersion, newVersion, transaction) {
    console.log(oldVersion)
  }
})

In questa callback puoi controllare da quale versione sta aggiornando l’utente ed eseguire operazioni diverse di conseguenza.

Puoi migrare da una versione precedente del database con questa sintassi

javascript
(async () => {
  //...
  const dbName="mydbname"
  const storeName="store0"
  const version = 1

const db = await openDB(dbName, version, {
    upgrade(db, oldVersion, newVersion, transaction) {
      switch (oldVersion) {
        case 0:
          // uno store introdotto nella versione 1
          db.createObjectStore('store1')
        case 1:
          // elimina il vecchio store nella versione 2, creane uno nuovo
          db.createObjectStore('store2', { keyPath: 'name' })
      }
      db.createObjectStore(storeName)
    }
  })
})()

Chiave univoca

createObjectStore()come vedi nel caso 1 accetta un secondo parametro che indica la chiave di indice del database. È molto utile quando archivi oggetti: le chiamate put() non hanno bisogno di un secondo parametro, ma possono prendere solo il valore (un oggetto) e la chiave verrà mappata sulla proprietà dell’oggetto che porta quel nome.

L’indice ti dà un modo per recuperare più tardi un valore tramite quella chiave specifica, e deve essere univoco (ogni elemento deve avere una chiave diversa).

Una chiave può essere impostata con incremento automatico, così non devi gestirla nel codice client:

javascript
db.createObjectStore('notes', { autoIncrement: true })

Usa l’incremento automatico se i tuoi valori non contengono già una chiave univoca.


Verificare l’esistenza di un object store

Puoi verificare se un object store esiste già chiamando il metodo objectStoreNames():

javascript
const storeName="store1"

if (!db.objectStoreNames.contains(storeName)) {
  db.createObjectStore(storeName)
}

Eliminare da IndexedDB

Eliminare il database, un object store e i dati

Eliminare un database

javascript
await deleteDB('mydb')

Eliminare un object store

Un object store può essere eliminato solo nella callback di apertura di un db, e quella callback viene chiamata solo se specifichi una versione superiore a quella installata:

javascript
const db = await openDB('dogsdb', 2, {
  upgrade(db, oldVersion, newVersion, transaction) {
    switch (oldVersion) {
      case 0:
        // uno store introdotto nella versione 1
        db.createObjectStore('store1')
      case 1:
        // elimina il vecchio store nella versione 2, creane uno nuovo
        db.deleteObjectStore('store1')
        db.createObjectStore('store2')
    }
  }
})

Per eliminare dati in un object store, usa una transazione

javascript
const key = 232

const db = await openDB(/*...*/)
const tx = await db.transaction('store', 'readwrite')
const store = await tx.objectStore('store')
await store.delete(key)
await tx.complete

JavaScript

Damien Flandrin Sviluppatore web dal 2010, creatore di Gekkode e di Email Impact. Ogni articolo è testato su un progetto reale prima della pubblicazione. Contatti
Newsletter

I nuovi test, tutorial e progetti, via e-mail.

Test riproducibili, codice versionato, risultati datati. Mai spam.