Utiliser IndexedDB

Ce guide présente les principes de base de l'API IndexedDB. Nous utilisons la bibliothèque IndexedDB Promised de Jake Archibald, qui est très semblable à l'API IndexedDB, mais qui utilise des promesses, que vous pouvez await pour une syntaxe plus concise. Cela simplifie l'API tout en conservant sa structure.

Qu'est-ce qu'IndexedDB ?

IndexedDB est un système de stockage NoSQL à grande échelle qui permet de stocker à peu près tout dans le navigateur de l'utilisateur. En plus des actions de recherche, d'obtention et d'insertion habituelles, IndexedDB accepte également les transactions et est bien adapté au stockage de grandes quantités de données structurées.

Chaque base de données IndexedDB est unique pour une origine (généralement le domaine ou le sous-domaine du site), ce qui signifie qu'elle ne peut pas accéder à une autre origine ni être accessible par une autre origine. Ses limites de stockage de données sont généralement élevées, voire inexistantes, mais les différents navigateurs gèrent les limites et l'éviction des données de manière différente. Pour en savoir plus, consultez la section Pour aller plus loin.

Termes IndexedDB

Base de données
Le niveau le plus élevé d'IndexedDB. Il contient les stores d'objets, qui contiennent à leur tour les données que vous souhaitez conserver. Vous pouvez créer plusieurs bases de données avec les noms de votre choix.
Store d'objets
Un bucket individuel pour stocker des données, semblable aux tables des bases de données relationnelles. En règle générale, il existe un store d'objets pour chaque type (pas le type de données JavaScript) de données que vous stockez. Contrairement aux tables de base de données, les types de données JavaScript d'un store n'ont pas besoin d'être cohérents. Par exemple, si une application dispose d'un store d'objets people contenant des informations sur trois personnes, les propriétés d'âge de ces personnes peuvent être 53, 'twenty-five' et unknown.
Index
Type de store d'objets permettant d'organiser les données d'un autre store d'objets (appelé store d'objets de référence) par une propriété individuelle des données. L'index permet de récupérer les enregistrements du store d'objets par cette propriété. Par exemple, si vous stockez des personnes, vous pouvez les récupérer ultérieurement par leur nom, leur âge ou leur animal préféré.
Opération
Une interaction avec la base de données.
Transaction
Wrapper autour d'une opération ou d'un groupe d'opérations qui garantit l'intégrité de la base de données. Si l'une des actions d'une transaction échoue, aucune d'entre elles n'est appliquée et la base de données revient à l'état dans lequel elle se trouvait avant le début de la transaction. Toutes les opérations de lecture ou d'écriture dans IndexedDB doivent faire partie d'une transaction. Cela permet d'effectuer des opérations atomiques de lecture-modification-écriture sans risque de conflit avec d'autres threads agissant simultanément sur la base de données.
Cursor
Mécanisme permettant d'itérer sur plusieurs enregistrements d'une base de données.

Vérifier la compatibilité avec IndexedDB

IndexedDB est compatible avec presque tous les navigateurs. Toutefois, si vous travaillez avec des navigateurs plus anciens, il n'est pas inutile de détecter la compatibilité des fonctionnalités, juste au cas où. Le moyen le plus simple consiste à vérifier l'objet 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();

Ouvrir une base de données

Avec IndexedDB, vous pouvez créer plusieurs bases de données avec les noms de votre choix. Si une base de données n'existe pas lorsque vous essayez de l'ouvrir, elle est créée automatiquement. Pour ouvrir une base de données, utilisez la méthode openDB() de la bibliothèque 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();

Cette méthode renvoie une promesse qui se résout en un objet de base de données. Lorsque vous utilisez la méthode openDB(), indiquez un nom, un numéro de version et un objet d'événements pour configurer la base de données.

Voici un exemple de méthode openDB() en contexte :

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

Placez la vérification de la compatibilité avec IndexedDB en haut de la fonction anonyme. Cela permet de quitter la fonction si le navigateur n'est pas compatible avec IndexedDB. Si la fonction peut continuer, elle appelle la méthode openDB() pour ouvrir une base de données nommée 'test-db1'. Dans cet exemple, l'objet d'événements facultatif a été omis pour simplifier les choses, mais vous devez le spécifier pour effectuer un travail significatif avec IndexedDB.

Utiliser des stores d'objets

Une base de données IndexedDB contient un ou plusieurs stores d'objets, chacun comportant une colonne pour une clé et une autre colonne pour les données associées à cette clé.

Créer des stores d'objets

Une base de données IndexedDB bien structurée doit comporter un store d'objets pour chaque type de données à conserver. Par exemple, un site qui conserve les profils utilisateur et les notes peut disposer d'un store d'objets people contenant des objets person et d'un store d'objets notes contenant des objets note.

Pour garantir l'intégrité de la base de données, vous ne pouvez créer ou supprimer des stores d'objets dans l'objet d'événements que lors d'un appel openDB(). L'objet d'événements expose une méthode upgrade() qui vous permet de créer des stores d'objets. Appelez la méthode createObjectStore() dans la méthode upgrade() pour créer le store d'objets :

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

Cette méthode utilise le nom du store d'objets et un objet de configuration facultatif qui vous permet de définir différentes propriétés pour le store d'objets.

Voici un exemple d'utilisation de 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();

Dans cet exemple, un objet d'événements est transmis à la méthode openDB() pour créer le store d'objets. Comme précédemment, la création du store d'objets est effectuée dans la méthode upgrade() de l'objet d'événement. Toutefois, comme le navigateur renvoie une erreur si vous essayez de créer un store d'objets qui existe déjà, nous vous recommandons d'encapsuler la méthode createObjectStore() dans une instruction if qui vérifie si le store d'objets existe. Dans le bloc if, appelez createObjectStore() pour créer un store d'objets nommé 'firstOS'.

Comment définir des clés primaires

Lorsque vous définissez des stores d'objets, vous pouvez définir la manière dont les données sont identifiées de manière unique dans le store à l'aide d'une clé primaire. Vous pouvez définir une clé primaire en définissant un chemin d'accès de clé ou en utilisant un générateur de clés.

Un chemin de clé est une propriété qui existe toujours et qui contient une valeur unique. Par exemple, dans le cas d'un store d'objets people, vous pouvez choisir l'adresse e-mail comme chemin de clé :

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

Cet exemple crée un store d'objets appelé 'people' et attribue la propriété email comme clé primaire dans l'option keyPath.

Vous pouvez également utiliser un générateur de clés tel que autoIncrement. Le générateur de clés crée une valeur unique pour chaque objet ajouté au store d'objets. Par défaut, si vous ne spécifiez pas de clé, IndexedDB en crée une et la stocke séparément des données.

L'exemple suivant crée un store d'objets appelé 'notes' et définit la clé primaire à attribuer automatiquement en tant que nombre à incrémentation automatique :

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'exemple suivant est semblable au précédent, mais cette fois, la valeur d'incrémentation automatique est explicitement attribuée à une propriété nommée '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();

Le choix de la méthode à utiliser pour définir la clé dépend de vos données. Si vos données ont une propriété qui est toujours unique, vous pouvez en faire la keyPath pour appliquer cette unicité. Sinon, utilisez une valeur à incrémentation automatique.

Le code suivant crée trois stores d'objets illustrant les différentes manières de définir des clés primaires dans les stores d'objets :

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

Définir des index

Les index sont un type de store d'objets utilisé pour récupérer des données à partir du store d'objets de référence par une propriété spécifiée. Un index se trouve dans le store d'objets de référence et contient les mêmes données, mais utilise la propriété spécifiée comme chemin d'accès de clé au lieu de la clé primaire du store de référence. Les index doivent être créés lorsque vous créez vos stores d'objets. Ils peuvent être utilisés pour définir une contrainte unique sur vos données.

Pour créer un index, appelez la méthode createIndex() sur une instance de store d'objets :

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

Cette méthode crée et renvoie un objet d'index. La méthode createIndex() sur l'instance du store d'objets utilise le nom du nouvel index comme premier argument, et le deuxième argument fait référence à la propriété des données que vous souhaitez indexer. Le dernier argument vous permet de définir deux options qui déterminent le fonctionnement de l'index : unique et multiEntry. Si unique est défini sur true, l'index n'autorise pas les valeurs en double pour une même clé. Ensuite, multiEntry détermine le comportement de createIndex() lorsque la propriété indexée est un tableau. Si elle est définie sur true, createIndex() ajoute une entrée dans l'index pour chaque élément du tableau. Sinon, elle ajoute une seule entrée contenant le tableau.

Exemple :

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

Dans cet exemple, les stores d'objets 'people' et 'notes' ont des index. Pour créer les index, commencez par attribuer le résultat de createObjectStore() (un objet de store d'objets) à une variable afin de pouvoir appeler createIndex() dessus.

Utiliser les données

Cette section explique comment créer, lire, mettre à jour et supprimer des données. Ces opérations sont toutes asynchrones et utilisent des promesses là où l'API IndexedDB utilise des requêtes. Cela simplifie l'API. Au lieu d'écouter les événements déclenchés par la requête, vous pouvez appeler .then() sur l'objet de base de données renvoyé par la méthode openDB() pour commencer à interagir avec la base de données, ou await sa création.

Toutes les opérations de données dans IndexedDB sont effectuées dans une transaction. Chaque opération se présente sous la forme suivante :

  1. Obtenir un objet de base de données.
  2. Ouvrir une transaction sur la base de données.
  3. Ouvrir le store d'objets lors de la transaction.
  4. Effectuer une opération sur le store d'objets.

Une transaction peut être considérée comme un wrapper sécurisé autour d'une opération ou d'un groupe d'opérations. Si l'une des actions d'une transaction échoue, toutes les actions sont annulées. Les transactions sont spécifiques à un ou plusieurs stores d'objets, que vous définissez lorsque vous ouvrez la transaction. Ils peuvent être en lecture seule ou en lecture et écriture. Cela indique si les opérations de la transaction lisent les données ou apportent une modification à la base de données.

Créer des données

Pour créer des données, appelez la méthode add() sur l'instance de base de données et transmettez les données que vous souhaitez ajouter. Le premier argument de la méthode add() est le store d'objets auquel vous souhaitez ajouter les données, et le deuxième argument est un objet contenant les champs et les données associées que vous souhaitez ajouter. Voici l'exemple le plus simple, où une seule ligne de données est ajoutée :

import {openDB} from 'idb';

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

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

addItemToStore();

Chaque appel add() se produit dans une transaction. Par conséquent, même si la promesse est résolue, cela ne signifie pas nécessairement que l'opération a fonctionné. Pour vous assurer que l'opération d'ajout a été effectuée, vous devez vérifier si l'ensemble de la transaction est terminé à l'aide de la méthode transaction.done(). Il s'agit d'une promesse qui se résout lorsque la transaction se termine et qui est rejetée si la transaction échoue. Vous devez effectuer cette vérification pour toutes les opérations d'écriture, car c'est le seul moyen de savoir si les modifications apportées à la base de données ont bien été effectuées.

Le code suivant montre l'utilisation de la méthode add() dans une transaction :

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

Une fois que vous avez ouvert la base de données (et créé un store d'objets si nécessaire), vous devez ouvrir une transaction en appelant la méthode transaction() sur celle-ci. Cette méthode utilise un argument pour le magasin sur lequel vous souhaitez effectuer une transaction, ainsi que le mode. Dans ce cas, nous souhaitons écrire dans le magasin. Cet exemple spécifie donc 'readwrite'.

L'étape suivante consiste à ajouter des articles au magasin dans le cadre de la transaction. Dans l'exemple précédent, nous avons affaire à trois opérations sur le magasin 'foods', qui renvoient chacune une promesse :

  1. Ajout d'un enregistrement pour un délicieux sandwich.
  2. Ajout d'un enregistrement pour des œufs.
  3. Signaler que la transaction est terminée (tx.done).

Comme toutes ces actions sont basées sur des promesses, nous devons attendre qu'elles soient toutes terminées. Transmettre ces promesses à Promise.all est un moyen pratique et ergonomique d'y parvenir. Promise.all accepte un tableau de promesses et se termine lorsque toutes les promesses qui lui ont été transmises ont été résolues.

Pour les deux enregistrements ajoutés, l'interface store de l'instance de transaction appelle add() et lui transmet les données. Vous pouvez await l'appel Promise.all pour qu'il se termine une fois la transaction terminée.

Lire des données

Pour lire des données, appelez la méthode get() sur l'instance de base de données que vous récupérez à l'aide de la méthode openDB(). get() prend le nom du magasin et la valeur de la clé primaire de l'objet que vous souhaitez récupérer. Voici un exemple simple :

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

Comme pour add(), la méthode get() renvoie une promesse. Vous pouvez donc l'await si vous préférez, ou utiliser le rappel .then() de la promesse.

L'exemple suivant utilise la méthode get() sur le store d'objets 'foods' de la base de données 'test-db4' pour obtenir une seule ligne à l'aide de la clé primaire '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();

La récupération d'une seule ligne de la base de données est assez simple : ouvrez la base de données, puis spécifiez le store d'objets et la valeur de clé primaire de la ligne dont vous souhaitez obtenir les données. Comme la méthode get() renvoie une promesse, vous pouvez l'await.

Mettre à jour des données

Pour mettre à jour des données, appelez la méthode put() sur le store d'objets. La méthode put() est semblable à la méthode add() et peut également être utilisée à la place de add() pour créer des données. Voici un exemple simple d'utilisation de put() pour mettre à jour une ligne dans un store d'objets en fonction de sa valeur de clé primaire :

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

Comme les autres méthodes, celle-ci renvoie une promesse. Vous pouvez également utiliser put() dans le cadre d'une transaction. Voici un exemple utilisant le magasin 'foods' de l'exemple précédent, qui met à jour le prix du sandwich et des œufs :

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 façon dont les éléments sont mis à jour dépend de la façon dont vous définissez une clé. Si vous définissez un keyPath, chaque ligne du store d'objets est associée à une clé intégrée. L'exemple précédent met à jour les lignes en fonction de cette clé. Lorsque vous mettez à jour des lignes dans cette situation, vous devez spécifier cette clé pour mettre à jour l'élément approprié dans le store d'objets. Vous pouvez également créer une clé hors ligne en définissant un autoIncrement comme clé primaire.

Supprimer des données

Pour supprimer des données, appelez la méthode delete() sur le store d'objets :

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

Comme pour add() et put(), vous pouvez l'utiliser dans le cadre d'une transaction :

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 structure de l'interaction avec la base de données est la même que pour les autres opérations. N'oubliez pas de vérifier que l'ensemble de la transaction est terminé en incluant la méthode tx.done dans le tableau que vous transmettez à Promise.all.

Récupérer toutes les données

Jusqu'à présent, vous n'avez récupéré qu'un seul objet à la fois dans le magasin. Vous pouvez également récupérer toutes les données, ou un sous-ensemble, à partir d'un store d'objets ou d'un index à l'aide de la méthode getAll() ou de curseurs.

La méthode getAll()

Le moyen le plus simple de récupérer toutes les données d'un store d'objets consiste à appeler getAll() sur le store d'objets ou l'index, comme suit :

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

Cette méthode renvoie tous les objets du store d'objets, sans aucune contrainte. C'est le moyen le plus direct d'obtenir toutes les valeurs d'un store d'objets, mais aussi le moins flexible.

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

Cet exemple appelle getAll() sur le store d'objets 'foods'. Cette opération renvoie tous les objets de 'foods', classés par clé primaire.

Utiliser des curseurs

Les curseurs offrent un moyen plus flexible de récupérer plusieurs objets. Un curseur sélectionne un par un chaque objet d'un store d'objets ou d'un index, ce qui vous permet d'effectuer une opération sur les données lorsqu'elles sont sélectionnées. Les curseurs, comme les autres opérations de base de données, fonctionnent dans les transactions.

Pour créer un curseur, appelez openCursor() sur le store d'objets dans le cadre d'une transaction. En utilisant le store 'foods' des exemples précédents, voici comment faire avancer un curseur dans toutes les lignes de données d'un store d'objets :

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

Dans ce cas, la transaction est ouverte en mode 'readonly' et sa méthode openCursor est appelée. Dans une boucle while ultérieure, les propriétés key et value de la ligne à la position actuelle du curseur peuvent être lues, et vous pouvez utiliser ces valeurs de la manière la plus logique pour votre application. Lorsque vous êtes prêt, vous pouvez appeler la méthode continue() de l'objet cursor pour passer à la ligne suivante. La boucle while se termine lorsque le curseur atteint la fin de l'ensemble de données.

Utiliser des curseurs avec des plages et des index

Les index vous permettent de récupérer les données d'un store d'objets par une propriété autre que la clé primaire. Vous pouvez créer un index sur n'importe quelle propriété, qui devient la keyPath de l'index, spécifier une plage sur cette propriété et obtenir les données de la plage à l'aide de getAll() ou d'un curseur.

Définissez votre plage à l'aide de l'objet IDBKeyRange et de l'une des méthodes suivantes :

Les méthodes upperBound() et lowerBound() spécifient les limites supérieure et inférieure de la plage.

IDBKeyRange.lowerBound(indexKey);

soit :

IDBKeyRange.upperBound(indexKey);

Chacune d'elles accepte un argument : la valeur keyPath de l'index pour l'élément que vous souhaitez spécifier comme limite supérieure ou inférieure.

La méthode bound() spécifie à la fois une limite supérieure et une limite inférieure :

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

La plage de ces fonctions est inclusive par défaut, ce qui signifie qu'elle inclut les données spécifiées comme limites de la plage. Pour exclure ces valeurs, spécifiez la plage comme exclusive en transmettant true comme deuxième argument pour lowerBound() ou upperBound(), ou comme troisième et quatrième arguments de bound(), pour les limites inférieure et supérieure respectivement.

L'exemple suivant utilise un index sur la propriété 'price' dans le store d'objets 'foods'. Le store est désormais associé à un formulaire comportant deux entrées pour les limites supérieure et inférieure de la plage. Utilisez le code suivant pour trouver des aliments dont le prix se situe entre ces limites :

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

L'exemple de code obtient d'abord les valeurs des limites et vérifie si elles existent. Le bloc de code suivant détermine la méthode à utiliser pour limiter la plage en fonction des valeurs. Dans l'interaction avec la base de données, ouvrez le store d'objets sur la transaction comme d'habitude, puis ouvrez l'index 'price' sur le store d'objets. L'index 'price' vous permet de rechercher des articles par prix.

Le code ouvre ensuite un curseur sur l'index et transmet la plage. Le curseur renvoie une promesse représentant le premier objet de la plage, ou undefined s'il n'y a pas de données dans la plage. La méthode cursor.continue() renvoie un curseur représentant l'objet suivant et continue la boucle jusqu'à ce que vous atteigniez la fin de la plage.

Gestion des versions de base de données

Lorsque vous appelez la méthode openDB(), vous pouvez spécifier le numéro de version de la base de données dans le deuxième paramètre. Dans tous les exemples de ce guide, la version a été définie sur 1, mais une base de données peut être mise à niveau vers une nouvelle version si vous devez la modifier d'une manière ou d'une autre. Si la version spécifiée est supérieure à celle de la base de données existante, le rappel upgrade de l'objet d'événement s'exécute, ce qui vous permet d'ajouter de nouveaux stores d'objets et index à la base de données.

L'objet db du rappel upgrade possède une propriété oldVersion spéciale, qui indique le numéro de version de la base de données à laquelle le navigateur a accès. Vous pouvez transmettre ce numéro de version dans une instruction switch pour exécuter des blocs de code dans le rappel upgrade en fonction du numéro de version de la base de données existante. Exemple :

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

Cet exemple définit la version la plus récente de la base de données sur 2. Lors de la première exécution de ce code, la base de données n'existe pas encore dans le navigateur. Par conséquent, oldVersion est 0 et l'instruction switch commence à case 0. Dans l'exemple, cela ajoute un store d'objets 'store' à la base de données.

Point clé : Dans les instructions switch, il y a généralement un break après chaque bloc case, mais il n'est délibérément pas utilisé ici. Ainsi, si la base de données existante est en retard de quelques versions ou si elle n'existe pas, le code continue de s'exécuter dans le reste des blocs case jusqu'à ce qu'elle soit à jour. Ainsi, dans l'exemple, le navigateur continue l'exécution via case 1, en créant un index name sur le store d'objets store.

Pour créer un index 'description' sur le store d'objets 'store', mettez à jour le numéro de version et ajoutez un nouveau bloc case comme suit :

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

Si la base de données que vous avez créée dans l'exemple précédent existe toujours dans le navigateur, lorsque cette commande s'exécute, oldVersion est 2. Le navigateur ignore case 0 et case 1, et exécute le code dans case 2, ce qui crée un index description. Ensuite, le navigateur dispose d'une base de données en version 3 contenant un store d'objets store avec les index name et description.

Documentation complémentaire

Les ressources suivantes fournissent plus d'informations et de contexte sur l'utilisation d'IndexedDB.

Documentation IndexedDB

Limites de stockage des données