Utilizza IndexedDB

Questa guida illustra le nozioni di base dell'API IndexedDB. Utilizziamo la libreria IndexedDB Promised di Jake Archibald, che è molto simile all'API IndexedDB, ma utilizza le promise, che puoi await per una sintassi più concisa. In questo modo l'API viene semplificata, mantenendo la sua struttura.

Che cos'è IndexedDB?

IndexedDB è un sistema di archiviazione NoSQL su larga scala che consente di archiviare praticamente qualsiasi cosa nel browser dell'utente. Oltre alle consuete azioni di ricerca, recupero e inserimento, IndexedDB supporta anche le transazioni ed è ideale per archiviare grandi quantità di dati strutturati.

Ogni database IndexedDB è univoco per un'origine (in genere il dominio o il sottodominio del sito), il che significa che non può accedere ad altre origini né essere accessibile da altre origini. I suoi limiti di archiviazione dei dati sono generalmente ampi, se esistono, ma browser diversi gestiscono i limiti e l'eliminazione dei dati in modo diverso. Per saperne di più, consulta la sezione Per saperne di più.

Termini di IndexedDB

Database
Il livello più alto di IndexedDB. Contiene gli archivi di oggetti, che a loro volta contengono i dati che vuoi rendere persistenti. Puoi creare più database con i nomi che preferisci.
Archivio di oggetti
Un singolo bucket per archiviare i dati, simile alle tabelle nei database relazionali. In genere, esiste un archivio di oggetti per ogni tipo (non tipo di dati JavaScript) di dati che stai archiviando. A differenza delle tabelle di database, i tipi di dati JavaScript in un archivio non devono essere coerenti. Ad esempio, se un'app ha un archivio di oggetti people contenente informazioni su tre persone, le proprietà relative all'età di queste persone potrebbero essere 53, 'twenty-five' e unknown.
Index
Un tipo di archivio di oggetti per organizzare i dati in un altro archivio di oggetti (chiamato archivio di oggetti di riferimento) in base a una singola proprietà dei dati. L'indice viene utilizzato per recuperare i record nell'archivio di oggetti in base a questa proprietà. Ad esempio, se memorizzi persone, potresti volerle recuperare in un secondo momento in base al nome, all'età o all'animale preferito.
Operazione
Un'interazione con il database.
Transazione
Un wrapper attorno a un'operazione o a un gruppo di operazioni che garantisce l'integrità del database. Se una delle azioni di una transazione non va a buon fine, nessuna viene applicata e il database torna allo stato in cui si trovava prima dell'inizio della transazione. Tutte le operazioni di lettura o scrittura in IndexedDB devono far parte di una transazione. In questo modo, è possibile eseguire operazioni atomiche di lettura-modifica-scrittura senza il rischio di conflitti con altri thread che agiscono contemporaneamente sul database.
Cursore
Un meccanismo per eseguire l'iterazione su più record in un database.

Come verificare il supporto di IndexedDB

IndexedDB è supportato quasi universalmente. Tuttavia, se utilizzi browser meno recenti, è consigliabile rilevare il supporto delle funzionalità per sicurezza. Il modo più semplice è controllare l'oggetto window:

function indexedDBStuff () {
  // Check for IndexedDB support:
  if (!('indexedDB' in window)) {
    // Can't use IndexedDB
    console.log("This browser doesn't support IndexedDB");
    return;
  } else {
    // Do IndexedDB stuff here:
    // ...
  }
}

// Run IndexedDB code:
indexedDBStuff();

Come aprire un database

Con IndexedDB, puoi creare più database con i nomi che preferisci. Se un database non esiste quando provi ad aprirlo, viene creato automaticamente. Per aprire un database, utilizza il metodo openDB() della libreria idb:

import {openDB} from 'idb';

async function useDB () {
  // Returns a promise, which makes `idb` usable with async-await.
  const dbPromise = await openDB('example-database', version, events);
}

useDB();

Questo metodo restituisce una promessa che si risolve in un oggetto di database. Quando utilizzi il metodo openDB(), fornisci un nome, un numero di versione e un oggetto events per configurare il database.

Ecco un esempio del metodo openDB() nel contesto:

import {openDB} from 'idb';

async function useDB () {
  // Opens the first version of the 'test-db1' database.
  // If the database does not exist, it will be created.
  const dbPromise = await openDB('test-db1', 1);
}

useDB();

Posiziona il controllo del supporto di IndexedDB nella parte superiore della funzione anonima. In questo modo, la funzione viene chiusa se il browser non supporta IndexedDB. Se la funzione può continuare, chiama il metodo openDB() per aprire un database denominato 'test-db1'. In questo esempio, l'oggetto facoltativo events è stato omesso per semplicità, ma devi specificarlo per eseguire qualsiasi operazione significativa con IndexedDB.

Come utilizzare gli archivi di oggetti

Un database IndexedDB contiene uno o più archivi di oggetti, ognuno dei quali ha una colonna per una chiave e un'altra colonna per i dati associati a quella chiave.

Crea archivi di oggetti

Un database IndexedDB ben strutturato deve avere un archivio di oggetti per ogni tipo di dati che deve essere reso persistente. Ad esempio, un sito che conserva i profili utente e le note potrebbe avere un archivio di oggetti people contenente oggetti person e un archivio di oggetti notes contenente oggetti note.

Per garantire l'integrità del database, puoi creare o rimuovere archivi di oggetti nell'oggetto events solo in una chiamata openDB(). L'oggetto events espone un metodo upgrade() che consente di creare archivi di oggetti. Chiama il metodo createObjectStore() all'interno del metodo upgrade() per creare l'archivio di oggetti:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('example-database', 1, {
    upgrade (db) {
      // Creates an object store:
      db.createObjectStore('storeName', options);
    }
  });
}

createStoreInDB();

Questo metodo accetta il nome dell'archivio di oggetti e un oggetto di configurazione facoltativo che consente di definire varie proprietà per l'archivio di oggetti.

Di seguito è riportato un esempio di come utilizzare createObjectStore():

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db1', 1, {
    upgrade (db) {
      console.log('Creating a new object store...');

      // Checks if the object store exists:
      if (!db.objectStoreNames.contains('people')) {
        // If the object store does not exist, create it:
        db.createObjectStore('people');
      }
    }
  });
}

createStoreInDB();

In questo esempio, un oggetto events viene passato al metodo openDB() per creare l'archivio di oggetti e, come prima, il lavoro di creazione dell'archivio di oggetti viene eseguito nel metodo upgrade() dell'oggetto event. Tuttavia, poiché il browser genera un errore se provi a creare un archivio di oggetti già esistente, ti consigliamo di inserire il metodo createObjectStore() in un'istruzione if che controlli se l'archivio di oggetti esiste. All'interno del blocco if, chiama createObjectStore() per creare un archivio di oggetti denominato 'firstOS'.

Come definire le chiavi primarie

Quando definisci gli archivi di oggetti, puoi definire il modo in cui i dati vengono identificati in modo univoco nell'archivio utilizzando una chiave primaria. Puoi definire una chiave primaria definendo un percorso della chiave o utilizzando un generatore di chiavi.

Un percorso chiave è una proprietà che esiste sempre e contiene un valore univoco. Ad esempio, nel caso di un archivio di oggetti people, puoi scegliere l'indirizzo email come percorso della chiave:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }
    }
  });
}

createStoreInDB();

Questo esempio crea un archivio di oggetti denominato 'people' e assegna la proprietà email come chiave primaria nell'opzione keyPath.

Puoi anche utilizzare un generatore di chiavi come autoIncrement. Il generatore di chiavi crea un valore univoco per ogni oggetto aggiunto all'archivio di oggetti. Per impostazione predefinita, se non specifichi una chiave, IndexedDB ne crea una e la archivia separatamente dai dati.

L'esempio seguente crea un archivio di oggetti denominato 'notes' e imposta la chiave primaria in modo che venga assegnata automaticamente come numero con incremento automatico:

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

L'esempio seguente è simile a quello precedente, ma questa volta il valore con incremento automatico viene assegnato esplicitamente a una proprietà denominata 'id'.

import {openDB} from 'idb';

async function createStoreInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoreInDB();

La scelta del metodo da utilizzare per definire la chiave dipende dai dati. Se i tuoi dati hanno una proprietà sempre univoca, puoi impostarla come keyPath per applicare questa unicità. In caso contrario, utilizza un valore con incremento automatico.

Il codice seguente crea tre archivi di oggetti che mostrano i vari modi per definire le chiavi primarie negli archivi di oggetti:

import {openDB} from 'idb';

async function createStoresInDB () {
  const dbPromise = await openDB('test-db2', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        db.createObjectStore('people', { keyPath: 'email' });
      }

      if (!db.objectStoreNames.contains('notes')) {
        db.createObjectStore('notes', { autoIncrement: true });
      }

      if (!db.objectStoreNames.contains('logs')) {
        db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createStoresInDB();

Come definire gli indici

Gli indici sono un tipo di archivio di oggetti utilizzato per recuperare i dati dall'archivio di oggetti di riferimento in base a una proprietà specificata. Un indice si trova all'interno dell'archivio di oggetti di riferimento e contiene gli stessi dati, ma utilizza la proprietà specificata come percorso della chiave anziché la chiave primaria dell'archivio di riferimento. Gli indici devono essere creati quando crei gli archivi di oggetti e possono essere utilizzati per definire un vincolo univoco sui tuoi dati.

Per creare un indice, chiama il metodo createIndex() su un'istanza dell'archivio di oggetti:

import {openDB} from 'idb';

async function createIndexInStore() {
  const dbPromise = await openDB('storeName', 1, {
    upgrade (db) {
      const objectStore = db.createObjectStore('storeName');

      objectStore.createIndex('indexName', 'property', options);
    }
  });
}

createIndexInStore();

Questo metodo crea e restituisce un oggetto indice. Il metodo createIndex() nell'istanza dell'archivio di oggetti accetta il nome del nuovo indice come primo argomento, mentre il secondo argomento fa riferimento alla proprietà dei dati che vuoi indicizzare. L'argomento finale consente di definire due opzioni che determinano il funzionamento dell'indice: unique e multiEntry. Se unique è impostato su true, l'indice non consente valori duplicati per una singola chiave. Successivamente, multiEntry determina il comportamento di createIndex() quando la proprietà indicizzata è un array. Se è impostato su true, createIndex() aggiunge una voce nell'indice per ogni elemento dell'array. In caso contrario, aggiunge una singola voce contenente l'array.

Ecco un esempio:

import {openDB} from 'idb';

async function createIndexesInStores () {
  const dbPromise = await openDB('test-db3', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('people')) {
        const peopleObjectStore = db.createObjectStore('people', { keyPath: 'email' });

        peopleObjectStore.createIndex('gender', 'gender', { unique: false });
        peopleObjectStore.createIndex('ssn', 'ssn', { unique: true });
      }

      if (!db.objectStoreNames.contains('notes')) {
        const notesObjectStore = db.createObjectStore('notes', { autoIncrement: true });

        notesObjectStore.createIndex('title', 'title', { unique: false });
      }

      if (!db.objectStoreNames.contains('logs')) {
        const logsObjectStore = db.createObjectStore('logs', { keyPath: 'id', autoIncrement: true });
      }
    }
  });
}

createIndexesInStores();

In questo esempio, gli archivi di oggetti 'people' e 'notes' hanno indici. Per creare gli indici, assegna prima il risultato di createObjectStore() (un oggetto archivio di oggetti) a una variabile in modo da poter chiamare createIndex() su di essa.

Come utilizzare i dati

Questa sezione descrive come creare, leggere, aggiornare ed eliminare i dati. Queste operazioni sono tutte asincrone e utilizzano le promise dove l'API IndexedDB utilizza le richieste. In questo modo l'API viene semplificata. Anziché ascoltare gli eventi attivati dalla richiesta, puoi chiamare .then() sull'oggetto del database restituito dal metodo openDB() per avviare le interazioni con il database o await la sua creazione.

Tutte le operazioni sui dati in IndexedDB vengono eseguite all'interno di una transazione. Ogni operazione ha il seguente formato:

  1. Recupera l'oggetto di database.
  2. Apri la transazione sul database.
  3. Apri l'archivio di oggetti nella transazione.
  4. Esegui l'operazione sull'archivio di oggetti.

Una transazione può essere considerata un wrapper sicuro per un'operazione o un gruppo di operazioni. Se una delle azioni all'interno di una transazione non va a buon fine, viene eseguito il rollback di tutte le azioni. Le transazioni sono specifiche per uno o più archivi di oggetti, che definisci quando apri la transazione. Possono essere di sola lettura o di lettura e scrittura. Questo indica se le operazioni all'interno della transazione leggono i dati o apportano una modifica al database.

Crea dati

Per creare i dati, chiama il metodo add() nell'istanza del database e trasmetti i dati che vuoi aggiungere. Il primo argomento del metodo add() è l'archivio di oggetti a cui vuoi aggiungere i dati, mentre il secondo argomento è un oggetto contenente i campi e i dati associati che vuoi aggiungere. Ecco l'esempio più semplice, in cui viene aggiunta una singola riga di dati:

import {openDB} from 'idb';

async function addItemToStore () {
  const db = await openDB('example-database', 1);

  await db.add('storeName', {
    field: 'data'
  });
}

addItemToStore();

Ogni chiamata add() avviene all'interno di una transazione, quindi anche se la promise viene risolta correttamente, non significa necessariamente che l'operazione sia andata a buon fine. Per assicurarti che l'operazione di aggiunta sia stata eseguita, devi verificare se l'intera transazione è stata completata utilizzando il metodo transaction.done(). Si tratta di una promessa che viene risolta quando la transazione viene completata e rifiutata se la transazione genera errori. Devi eseguire questo controllo per tutte le operazioni di "scrittura", perché è l'unico modo per sapere se le modifiche al database sono state effettivamente apportate.

Il seguente codice mostra l'utilizzo del metodo add() all'interno di una transazione:

import {openDB} from 'idb';

async function addItemsToStore () {
  const db = await openDB('test-db4', 1, {
    upgrade (db) {
      if (!db.objectStoreNames.contains('foods')) {
        db.createObjectStore('foods', { keyPath: 'name' });
      }
    }
  });
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Add multiple items to the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.add({
      name: 'Sandwich',
      price: 4.99,
      description: 'A very tasty sandwich!',
      created: new Date().getTime(),
    }),
    tx.store.add({
      name: 'Eggs',
      price: 2.99,
      description: 'Some nice eggs you can cook up!',
      created: new Date().getTime(),
    }),
    tx.done
  ]);
}

addItemsToStore();

Dopo aver aperto il database (e creato un archivio di oggetti, se necessario), dovrai aprire una transazione chiamando il metodo transaction(). Questo metodo accetta un argomento per lo store su cui vuoi effettuare la transazione, nonché la modalità. In questo caso, ci interessa scrivere nello store, quindi questo esempio specifica 'readwrite'.

Il passaggio successivo consiste nell'iniziare ad aggiungere articoli allo store nell'ambito della transazione. Nell'esempio precedente, abbiamo a che fare con tre operazioni sullo store 'foods', ognuna delle quali restituisce una promise:

  1. Aggiunta di un record per un panino gustoso.
  2. Aggiungo un record per alcune uova.
  3. Segnala che la transazione è stata completata (tx.done).

Poiché tutte queste azioni sono basate su promise, dobbiamo attendere che vengano completate. Passare queste promise a Promise.all è un modo semplice ed ergonomico per farlo. Promise.all accetta un array di promesse e termina quando tutte le promesse passate sono state risolte.

Per i due record aggiunti, l'interfaccia store dell'istanza della transazione chiama add() e le passa i dati. Puoi utilizzare await per la chiamata Promise.all in modo che termini quando la transazione viene completata.

Lettura di dati

Per leggere i dati, chiama il metodo get() sull'istanza del database che recuperi utilizzando il metodo openDB(). get() accetta il nome dello store e il valore della chiave primaria dell'oggetto che vuoi recuperare. Ecco un esempio di base:

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('example-database', 1);

  // Get a value from the object store by its primary key value:
  const value = await db.get('storeName', 'unique-primary-key-value');
}

getItemFromStore();

Come per add(), il metodo get() restituisce una promise, quindi puoi utilizzare await se preferisci o utilizzare il callback .then() della promise.

L'esempio seguente utilizza il metodo get() nell'archivio di oggetti 'foods' del database 'test-db4' per recuperare una singola riga tramite la chiave primaria 'name':

import {openDB} from 'idb';

async function getItemFromStore () {
  const db = await openDB('test-db4', 1);
  const value = await db.get('foods', 'Sandwich');

  console.dir(value);
}

getItemFromStore();

Il recupero di una singola riga dal database è abbastanza semplice: apri il database e specifica l'archivio di oggetti e il valore della chiave primaria della riga da cui vuoi recuperare i dati. Poiché il metodo get() restituisce una promise, puoi utilizzare await.

Aggiorna i dati

Per aggiornare i dati, chiama il metodo put() nell'archivio di oggetti. Il metodo put() è simile al metodo add() e può essere utilizzato anche al posto di add() per creare dati. Ecco un esempio di base di utilizzo di put() per aggiornare una riga in un archivio di oggetti in base al valore della chiave primaria:

import {openDB} from 'idb';

async function updateItemInStore () {
  const db = await openDB('example-database', 1);

  // Update a value from in an object store with an inline key:
  await db.put('storeName', { inlineKeyName: 'newValue' });

  // Update a value from in an object store with an out-of-line key.
  // In this case, the out-of-line key value is 1, which is the
  // auto-incremented value.
  await db.put('otherStoreName', { field: 'value' }, 1);
}

updateItemInStore();

Come altri metodi, questo metodo restituisce una promise. Puoi anche utilizzare put() come parte di una transazione. Ecco un esempio di utilizzo del negozio 'foods' di prima che aggiorna il prezzo del panino e delle uova:

import {openDB} from 'idb';

async function updateItemsInStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Update multiple items in the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.put({
      name: 'Sandwich',
      price: 5.99,
      description: 'A MORE tasty sandwich!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.store.put({
      name: 'Eggs',
      price: 3.99,
      description: 'Some even NICER eggs you can cook up!',
      updated: new Date().getTime() // This creates a new field
    }),
    tx.done
  ]);
}

updateItemsInStore();

La modalità di aggiornamento degli elementi dipende da come imposti una chiave. Se imposti un keyPath, ogni riga nell'archivio di oggetti è associata a una chiave inline. L'esempio precedente aggiorna le righe in base a questa chiave e, quando aggiorni le righe in questa situazione, devi specificare la chiave per aggiornare l'elemento appropriato nell'archivio di oggetti. Puoi anche creare una chiave out-of-line impostando un autoIncrement come chiave primaria.

Elimina i dati

Per eliminare i dati, chiama il metodo delete() nell'archivio di oggetti:

import {openDB} from 'idb';

async function deleteItemFromStore () {
  const db = await openDB('example-database', 1);

  // Delete a value 
  await db.delete('storeName', 'primary-key-value');
}

deleteItemFromStore();

Come add() e put(), puoi utilizzarlo nell'ambito di una transazione:

import {openDB} from 'idb';

async function deleteItemsFromStore () {
  const db = await openDB('test-db4', 1);
  
  // Create a transaction on the 'foods' store in read/write mode:
  const tx = db.transaction('foods', 'readwrite');

  // Delete multiple items from the 'foods' store in a single transaction:
  await Promise.all([
    tx.store.delete('Sandwich'),
    tx.store.delete('Eggs'),
    tx.done
  ]);
}

deleteItemsFromStore();

La struttura dell'interazione con il database è la stessa delle altre operazioni. Ricordati di verificare che l'intera transazione sia stata completata includendo il metodo tx.done nell'array che passi a Promise.all.

Recupero di tutti i dati

Finora hai recuperato gli oggetti dallo store solo uno alla volta. Puoi anche recuperare tutti i dati o un sottoinsieme da un archivio di oggetti o un indice utilizzando il metodo getAll() o i cursori.

Il metodo getAll()

Il modo più semplice per recuperare tutti i dati di un archivio di oggetti è chiamare getAll() sull'archivio di oggetti o sull'indice, in questo modo:

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('storeName');

  console.dir(allValues);
}

getAllItemsFromStore();

Questo metodo restituisce tutti gli oggetti nell'archivio di oggetti, senza alcun vincolo. È il modo più diretto per ottenere tutti i valori da un archivio di oggetti, ma anche il meno flessibile.

import {openDB} from 'idb';

async function getAllItemsFromStore () {
  const db = await openDB('test-db4', 1);

  // Get all values from the designated object store:
  const allValues = await db.getAll('foods');

  console.dir(allValues);
}

getAllItemsFromStore();

Questo esempio chiama getAll() nell'archivio di oggetti 'foods'. Vengono restituiti tutti gli oggetti di 'foods', ordinati in base alla chiave primaria.

Come utilizzare i cursori

I cursori sono un modo più flessibile per recuperare più oggetti. Un cursore seleziona ogni oggetto in un archivio di oggetti o in un indice uno alla volta, consentendoti di eseguire un'operazione con i dati quando sono selezionati. I cursori, come le altre operazioni del database, funzionano nelle transazioni.

Per creare un cursore, chiama openCursor() nell'archivio di oggetti nell'ambito di una transazione. Utilizzando l'archivio 'foods' degli esempi precedenti, ecco come far avanzare un cursore in tutte le righe di dati di un archivio di oggetti:

import {openDB} from 'idb';

async function getAllItemsFromStoreWithCursor () {
  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');

  // Open a cursor on the designated object store:
  let cursor = await tx.store.openCursor();

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

getAllItemsFromStoreWithCursor();

In questo caso, la transazione viene aperta in modalità 'readonly' e viene chiamato il relativo metodo openCursor. In un ciclo while successivo, è possibile leggere le proprietà key e value della riga nella posizione corrente del cursore e puoi operare su questi valori nel modo più sensato per la tua app. Quando è tutto pronto, puoi chiamare il metodo continue() dell'oggetto cursor per passare alla riga successiva e il ciclo while termina quando il cursore raggiunge la fine del set di dati.

Utilizza i cursori con intervalli e indici

Gli indici ti consentono di recuperare i dati in un archivio di oggetti in base a una proprietà diversa dalla chiave primaria. Puoi creare un indice su qualsiasi proprietà, che diventa la keyPath per l'indice, specificare un intervallo su quella proprietà e ottenere i dati all'interno dell'intervallo utilizzando getAll() o un cursore.

Definisci l'intervallo utilizzando l'oggetto IDBKeyRange e uno dei seguenti metodi:

I metodi upperBound() e lowerBound() specificano i limiti superiore e inferiore dell'intervallo.

IDBKeyRange.lowerBound(indexKey);

Oppure:

IDBKeyRange.upperBound(indexKey);

Ognuno accetta un argomento: il valore keyPath dell'indice per l'elemento che vuoi specificare come limite superiore o inferiore.

Il metodo bound() specifica sia un limite superiore che un limite inferiore:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

Per impostazione predefinita, l'intervallo di queste funzioni è inclusivo, ovvero include i dati specificati come limiti dell'intervallo. Per escludere questi valori, specifica l'intervallo come esclusivo passando true come secondo argomento per lowerBound() o upperBound() oppure come terzo e quarto argomento di bound(), rispettivamente per i limiti inferiore e superiore.

L'esempio seguente utilizza un indice sulla proprietà 'price' nell'archivio di oggetti 'foods'. Ora al negozio è associato anche un modulo con due input per i limiti superiore e inferiore dell'intervallo. Utilizza il seguente codice per trovare alimenti con prezzi compresi tra questi limiti:

import {openDB} from 'idb';

async function searchItems (lower, upper) {
  if (!lower === '' && upper === '') {
    return;
  }

  let range;

  if (lower !== '' && upper !== '') {
    range = IDBKeyRange.bound(lower, upper);
  } else if (lower === '') {
    range = IDBKeyRange.upperBound(upper);
  } else {
    range = IDBKeyRange.lowerBound(lower);
  }

  const db = await openDB('test-db4', 1);
  const tx = await db.transaction('foods', 'readonly');
  const index = tx.store.index('price');

  // Open a cursor on the designated object store:
  let cursor = await index.openCursor(range);

  if (!cursor) {
    return;
  }

  // Iterate on the cursor, row by row:
  while (cursor) {
    // Show the data in the row at the current cursor position:
    console.log(cursor.key, cursor.value);

    // Advance the cursor to the next row:
    cursor = await cursor.continue();
  }
}

// Get items priced between one and four dollars:
searchItems(1.00, 4.00);

Il codice di esempio recupera innanzitutto i valori dei limiti e verifica se esistono. Il blocco di codice successivo decide quale metodo utilizzare per limitare l'intervallo in base ai valori. Nell'interazione con il database, apri l'archivio di oggetti nella transazione come di consueto, quindi apri l'indice 'price' nell'archivio di oggetti. L'indice 'price' ti consente di cercare gli articoli in base al prezzo.

Il codice apre quindi un cursore sull'indice e passa l'intervallo. Il cursore restituisce una promise che rappresenta il primo oggetto nell'intervallo o undefined se non sono presenti dati nell'intervallo. Il metodo cursor.continue() restituisce un cursore che rappresenta l'oggetto successivo e continua il ciclo fino a raggiungere la fine dell'intervallo.

Controllo delle versioni del database

Quando chiami il metodo openDB(), puoi specificare il numero di versione del database nel secondo parametro. In tutti gli esempi di questa guida, la versione è stata impostata su 1, ma è possibile eseguire l'upgrade di un database a una nuova versione se devi modificarlo in qualche modo. Se la versione specificata è successiva alla versione del database esistente, viene eseguito il callback upgrade nell'oggetto evento, consentendoti di aggiungere nuovi archivi di oggetti e indici al database.

L'oggetto db nel callback upgrade ha una proprietà oldVersion speciale, che indica il numero di versione del database a cui il browser ha accesso. Puoi passare questo numero di versione a un'istruzione switch per eseguire blocchi di codice all'interno del callback upgrade in base al numero di versione del database esistente. Ecco un esempio:

import {openDB} from 'idb';

const db = await openDB('example-database', 2, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');
    }
  }
});

Questo esempio imposta la versione più recente del database su 2. Quando questo codice viene eseguito per la prima volta, il database non esiste ancora nel browser, quindi oldVersion è 0 e l'istruzione switch inizia da case 0. Nell'esempio, questo aggiunge un archivio di oggetti 'store' al database.

Punto chiave: nelle istruzioni switch, di solito c'è un break dopo ogni blocco case, ma qui non viene utilizzato intenzionalmente. In questo modo, se il database esistente è indietro di alcune versioni o se non esiste, il codice continua con il resto dei blocchi case fino a quando non viene aggiornato. Quindi, nell'esempio, il browser continua l'esecuzione tramite case 1, creando un indice name nell'archivio di oggetti store.

Per creare un indice 'description' nell'archivio di oggetti 'store', aggiorna il numero di versione e aggiungi un nuovo blocco case come segue:

import {openDB} from 'idb';

const db = await openDB('example-database', 3, {
  upgrade (db, oldVersion) {
    switch (oldVersion) {
      case 0:
        // Create first object store:
        db.createObjectStore('store', { keyPath: 'name' });

      case 1:
        // Get the original object store, and create an index on it:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('name', 'name');

      case 2:
        const tx = await db.transaction('store', 'readwrite');
        tx.store.createIndex('description', 'description');
    }
  }
});

Se il database creato nell'esempio precedente esiste ancora nel browser, quando viene eseguito, oldVersion è 2. Il browser salta case 0 e case 1 ed esegue il codice in case 2, che crea un indice description. Dopodiché, il browser ha un database alla versione 3 contenente un archivio di oggetti store con indici name e description.

Per approfondire

Le seguenti risorse forniscono ulteriori informazioni e contesto per l'utilizzo di IndexedDB.

Documentazione di IndexedDB

Limiti di archiviazione dei dati