Как работать с IndexedDB

В этом руководстве рассматриваются основы IndexedDB API. Мы используем библиотеку IndexedDB Promised Джейка Арчибальда (Jake Archibald), которая очень похожа на IndexedDB API, но использует обещания, которые можно await для более краткого синтаксиса. Это упрощает API, сохраняя его структуру.

Что такое IndexedDB?

IndexedDB – это крупномасштабная система хранения NoSQL, которая позволяет хранить практически любые данные в браузере пользователя. Помимо обычных действий поиска, получения и размещения, IndexedDB также поддерживает транзакции и хорошо подходит для хранения больших объемов структурированных данных.

Каждая база данных IndexedDB уникальна для источника (обычно это домен или субдомен сайта). Это означает, что она не может получить доступ к другому источнику или быть доступна из него. Ограничения на хранение данных обычно большие, если они вообще есть, но разные браузеры по-разному обрабатывают ограничения и удаление данных. Дополнительную информацию можно найти в разделе Дополнительные материалы.

Условия использования IndexedDB

Database
Верхний уровень IndexedDB. Она содержит хранилища объектов, которые, в свою очередь, содержат данные, которые вы хотите сохранить. Вы можете создать несколько баз данных с любыми названиями.
Хранилище объектов
Отдельный сегмент для хранения данных, похожий на таблицы в реляционных базах данных. Обычно для каждого типа данных (не типа данных JavaScript) создается отдельное хранилище объектов. В отличие от таблиц базы данных, типы данных JavaScript в хранилище не обязательно должны быть согласованными. Например, если в хранилище объектов people приложения содержится информация о трех людях, то свойства возраста этих людей могут быть равны 53, 'twenty-five' и unknown.
Индекс
Тип хранилища объектов, предназначенный для упорядочивания данных в другом хранилище объектов (называемом эталонным хранилищем объектов) по отдельному свойству данных. Индекс используется для извлечения записей в хранилище объектов по этому свойству. Например, если вы храните данные о людях, то можете извлекать их по имени, возрасту или любимому животному.
Операция
Взаимодействие с базой данных.
Транзакция
Оболочка для операции или группы операций, которая обеспечивает целостность базы данных. Если одно из действий в транзакции завершается ошибкой, ни одно из них не применяется, и база данных возвращается в состояние, в котором она находилась до начала транзакции. Все операции чтения и записи в IndexedDB должны быть частью транзакции. Это позволяет выполнять атомарные операции чтения, изменения и записи без риска конфликтов с другими потоками, одновременно работающими с базой данных.
Курсор
Механизм для итерации по нескольким записям в базе данных.

Как проверить поддержку IndexedDB

IndexedDB поддерживается практически везде. Однако если вы работаете со старыми браузерами, то на всякий случай стоит проверить поддержку. Проще всего проверить объект 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();

Как открыть базу данных

С помощью IndexedDB можно создать несколько баз данных с любыми названиями. Если база данных не существует, когда вы пытаетесь ее открыть, она создается автоматически. Чтобы открыть базу данных, используйте метод openDB() из библиотеки 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();

Этот метод возвращает объект Promise, который распознается как объект базы данных. При использовании метода openDB() укажите название, номер версии и объект событий, чтобы настроить базу данных.

Пример использования метода openDB():

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

Поместите проверку поддержки IndexedDB в начало анонимной функции. Если браузер не поддерживает IndexedDB, функция завершает работу. Если функция может продолжить работу, она вызывает метод openDB(), чтобы открыть базу данных с названием 'test-db1'. В этом примере объект необязательных событий был опущен для простоты, но вам нужно указать его, чтобы выполнить какую-либо значимую работу с IndexedDB.

Как работать с хранилищами объектов

База данных IndexedDB содержит одно или несколько хранилищ объектов, в каждом из которых есть столбец для ключа и столбец для данных, связанных с этим ключом.

Создание хранилищ объектов

Хорошо структурированная база данных IndexedDB должна иметь одно хранилище объектов для каждого типа данных, которые необходимо сохранить. Например, на сайте, где хранятся профили пользователей и заметки, может быть хранилище объектов people, содержащее объекты person, и хранилище объектов notes, содержащее объекты note.

Чтобы обеспечить целостность базы данных, вы можете создавать или удалять хранилища объектов только в объекте events в вызове openDB(). Объект events предоставляет метод upgrade(), который позволяет создавать хранилища объектов. Вызовите метод createObjectStore() внутри метода upgrade(), чтобы создать хранилище объектов:

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

Этот метод принимает название хранилища объектов и необязательный объект конфигурации, который позволяет определить различные свойства хранилища объектов.

Ниже приведен пример использования 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();

В этом примере объект событий передается методу openDB() для создания хранилища объектов, и, как и ранее, создание хранилища объектов выполняется в методе upgrade() объекта событий. Однако, поскольку браузер выдает ошибку, если вы пытаетесь создать хранилище объектов, которое уже существует, мы рекомендуем обернуть метод createObjectStore() в оператор if, который проверяет, существует ли хранилище объектов. В блоке if вызовите createObjectStore(), чтобы создать хранилище объектов с названием 'firstOS'.

Как определить первичные ключи

При определении хранилищ объектов вы можете указать, как данные уникально идентифицируются в хранилище с помощью первичного ключа. Первичный ключ можно определить, задав путь к ключу или используя генератор ключей.

Путь ключа – это свойство, которое всегда существует и содержит уникальное значение. Например, в случае с хранилищем объектов people в качестве пути ключа можно выбрать адрес электронной почты:

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

В этом примере создается хранилище объектов с названием 'people', а свойство email назначается первичным ключом в параметре keyPath.

Вы также можете использовать генератор ключей, например autoIncrement. Генератор ключей создает уникальное значение для каждого объекта, добавленного в хранилище объектов. По умолчанию, если вы не укажете ключ, IndexedDB создаст его и сохранит отдельно от данных.

В следующем примере создается хранилище объектов с именем 'notes' и настраивается автоматическое назначение первичного ключа в виде автоматически увеличивающегося числа:

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

Следующий пример похож на предыдущий, но в этом случае автоматически увеличивающееся значение явно присваивается свойству с именем '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();

Выбор метода зависит от ваших данных. Если у ваших данных есть свойство, которое всегда уникально, вы можете сделать его keyPath, чтобы обеспечить эту уникальность. В противном случае используйте автоматически увеличивающееся значение.

В следующем коде создаются три хранилища объектов, демонстрирующие различные способы определения первичных ключей в хранилищах объектов:

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

Как определить индексы

Индексы – это тип хранилища объектов, используемый для извлечения данных из хранилища объектов ссылок по указанному свойству. Индекс находится в хранилище объектов и содержит те же данные, но в качестве пути ключа использует указанное свойство, а не первичный ключ хранилища. Индексы должны создаваться при создании хранилищ объектов и могут использоваться для определения уникального ограничения на ваши данные.

Чтобы создать индекс, вызовите метод createIndex() для экземпляра хранилища объектов:

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

Этот метод создает и возвращает объект индекса. Метод createIndex() в экземпляре хранилища объектов принимает название нового индекса в качестве первого аргумента, а второй аргумент относится к свойству данных, которые вы хотите индексировать. Последний аргумент позволяет задать два параметра, определяющих работу индекса: unique и multiEntry. Если для параметра unique задано значение true, индекс не допускает повторяющихся значений для одного ключа. Затем multiEntry определяет, как ведет себя createIndex(), когда индексированное свойство является массивом. Если задано значение true, createIndex() добавляет в индекс запись для каждого элемента массива. В противном случае добавляется одна запись, содержащая массив.

Пример:

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

В этом примере хранилища объектов 'people' и 'notes' имеют индексы. Чтобы создать индексы, сначала присвойте результат createObjectStore() (объект хранилища объектов) переменной, чтобы вы могли вызвать для него createIndex().

Как работать с данными

В этом разделе описывается, как создавать, читать, обновлять и удалять данные. Все эти операции асинхронны и используют промисы, тогда как IndexedDB API использует запросы. Это упрощает работу с API. Вместо того чтобы прослушивать события, активируемые запросом, вы можете вызвать .then() для объекта базы данных, возвращенного методом openDB(), чтобы начать взаимодействие с базой данных или await ее создание.

Все операции с данными в IndexedDB выполняются в рамках транзакции. Каждая операция имеет следующий вид:

  1. Получить объект базы данных.
  2. Открытая транзакция в базе данных.
  3. Откройте хранилище объектов в транзакции.
  4. Выполнить операцию в хранилище объектов.

Транзакцию можно рассматривать как безопасную оболочку для операции или группы операций. Если одно из действий в транзакции не выполняется, все действия отменяются. Транзакции относятся к одному или нескольким хранилищам объектов, которые вы определяете при открытии транзакции. Они могут быть доступны только для чтения или для чтения и записи. Это означает, считывают ли операции внутри транзакции данные или вносят изменения в базу данных.

Создание данных

Чтобы создать данные, вызовите метод add() для экземпляра базы данных и передайте данные, которые хотите добавить. Первый аргумент метода add() – это хранилище объектов, в которое вы хотите добавить данные, а второй – объект, содержащий поля и связанные данные, которые вы хотите добавить. Вот самый простой пример, в котором добавляется одна строка данных:

import {openDB} from 'idb';

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

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

addItemToStore();

Каждый вызов add() происходит в рамках транзакции, поэтому даже если промис успешно разрешается, это не обязательно означает, что операция сработала. Чтобы убедиться, что операция добавления была выполнена, необходимо проверить, завершена ли вся транзакция, с помощью метода transaction.done(). Это объект Promise, который распознается, когда транзакция завершается, и отклоняется, если в транзакции возникает ошибка. Эту проверку необходимо выполнять для всех операций записи, поскольку это единственный способ узнать, действительно ли были внесены изменения в базу данных.

В следующем коде показано использование метода add() внутри транзакции:

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

После того как вы откроете базу данных (и при необходимости создадите хранилище объектов), вам нужно будет открыть транзакцию, вызвав для нее метод transaction(). Этот метод принимает аргумент для хранилища, в котором вы хотите выполнить транзакцию, а также режим. В данном случае мы хотим записать данные в хранилище, поэтому в примере указано 'readwrite'.

Следующий шаг – добавление товаров в магазин в рамках транзакции. В предыдущем примере мы имеем дело с тремя операциями в хранилище 'foods', каждая из которых возвращает объект Promise:

  1. Добавляю запись о вкусном сэндвиче.
  2. Добавляю запись о яйцах.
  3. Сигнал о том, что транзакция завершена (tx.done).

Поскольку все эти действия основаны на обещаниях, нам нужно дождаться их завершения. Передача этих обещаний в Promise.all – удобный и эргономичный способ сделать это. Promise.all принимает массив промисов и завершает работу, когда все переданные ему промисы разрешены.

Для двух добавляемых записей интерфейс store экземпляра транзакции вызывает add() и передает ему данные. Вы можете await вызов Promise.all, чтобы он завершался вместе с транзакцией.

Чтение данных

Чтобы прочитать данные, вызовите метод get() для экземпляра базы данных, полученного с помощью метода openDB(). get() принимает название хранилища и значение первичного ключа объекта, который вы хотите получить. Вот простой пример:

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

Как и add(), метод get() возвращает объект Promise, поэтому вы можете использовать await или обратный вызов .then().

В следующем примере используется метод get() в хранилище объектов 'foods' базы данных 'test-db4', чтобы получить одну строку по первичному ключу '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();

Получить одну строку из базы данных довольно просто: откройте базу данных и укажите хранилище объектов и значение первичного ключа строки, из которой вы хотите получить данные. Поскольку метод get() возвращает обещание, вы можете использовать await.

Обновление данных

Чтобы обновить данные, вызовите метод put() в хранилище объектов. Метод put() похож на метод add() и может использоваться вместо негоadd() для создания данных. Вот простой пример использования put() для обновления строки в хранилище объектов по значению первичного ключа:

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

Как и другие методы, этот метод возвращает обещание. Вы также можете использовать put() в рамках транзакции. Вот пример, в котором используется магазин 'foods', который обновляет цену на сэндвич и яйца:

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

Способ обновления элементов зависит от того, как вы задали ключ. Если вы зададите keyPath, каждая строка в хранилище объектов будет связана со встроенным ключом. В предыдущем примере строки обновляются на основе этого ключа. В такой ситуации вам нужно будет указать ключ, чтобы обновить нужный элемент в хранилище объектов. Вы также можете создать внешний ключ, задав autoIncrement в качестве первичного ключа.

Удалить данные

Чтобы удалить данные, вызовите метод delete() в хранилище объектов:

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

Как и add() и put(), этот параметр можно использовать в транзакции:

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

Структура взаимодействия с базой данных такая же, как и для других операций. Не забудьте проверить, завершена ли транзакция, включив метод tx.done в массив, который вы передаете в Promise.all.

Получение всех данных

До сих пор вы извлекали объекты из хранилища по одному. Вы также можете получить все данные или их подмножество из хранилища объектов или индекса, используя метод getAll() или курсоры.

Метод getAll()

Проще всего получить все данные из хранилища объектов, вызвав метод getAll() для хранилища или индекса, например:

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

Этот метод возвращает все объекты в хранилище объектов без каких-либо ограничений. Это самый прямой способ получить все значения из хранилища объектов, но и наименее гибкий.

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

В этом примере вызывается getAll() в хранилище объектов 'foods'. Возвращает все объекты из 'foods', упорядоченные по первичному ключу.

Как использовать курсоры

Курсоры – это более гибкий способ получения нескольких объектов. Курсор выбирает каждый объект в хранилище объектов или индексе по одному, позволяя вам что-то делать с данными, когда они выбраны. Курсоры, как и другие операции с базами данных, работают в транзакциях.

Чтобы создать курсор, вызовите openCursor() в хранилище объектов в рамках транзакции. Ниже показано, как перемещать курсор по всем строкам данных в хранилище объектов на примере хранилища 'foods' из предыдущих примеров.

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

В этом случае транзакция открывается в режиме 'readonly' и вызывается ее метод openCursor. В последующем цикле while можно прочитать свойства key и value строки в текущей позиции курсора и использовать эти значения в приложении. Затем можно вызвать метод continue() объекта cursor, чтобы перейти к следующей строке. Цикл while завершается, когда курсор достигает конца набора данных.

Как использовать курсоры с диапазонами и индексами

Индексы позволяют получать данные в хранилище объектов по свойству, отличному от первичного ключа. Вы можете создать индекс для любого свойства, которое станет keyPath для индекса, указать диапазон для этого свойства и получить данные в пределах диапазона с помощью getAll() или курсора.

Определите диапазон с помощью объекта IDBKeyRange и любого из следующих методов:

Методы upperBound() и lowerBound() задают верхний и нижний пределы диапазона.

IDBKeyRange.lowerBound(indexKey);

Альтернативный вариант:

IDBKeyRange.upperBound(indexKey);

Каждый из них принимает один аргумент: значение keyPath индекса для элемента, который вы хотите указать в качестве верхнего или нижнего предела.

Метод bound() задает верхний и нижний пределы:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

По умолчанию диапазон для этих функций является инклюзивным, то есть включает данные, указанные в качестве его границ. Чтобы исключить эти значения, укажите диапазон как исключающий, передав true в качестве второго аргумента для lowerBound() или upperBound() или в качестве третьего и четвертого аргументов bound() для нижнего и верхнего пределов соответственно.

В следующем примере используется индекс для свойства 'price' в хранилище объектов 'foods'. Теперь к магазину также прикреплена форма с двумя входами для верхнего и нижнего пределов диапазона. Используйте следующий код, чтобы найти блюда с ценами в этих пределах:

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

В примере кода сначала запрашиваются значения лимитов и проверяется, существуют ли они. Следующий блок кода определяет, какой метод использовать для ограничения диапазона на основе значений. При взаимодействии с базой данных откройте хранилище объектов в транзакции, как обычно, а затем откройте индекс 'price' в хранилище объектов. Индекс 'price' позволяет искать товары по цене.

Затем код открывает курсор в индексе и передает диапазон. Курсор возвращает промис, представляющий первый объект в диапазоне, или undefined, если в диапазоне нет данных. Метод cursor.continue() возвращает курсор, представляющий следующий объект, и продолжает цикл, пока не будет достигнут конец диапазона.

Управление версиями баз данных

При вызове метода openDB() вы можете указать номер версии базы данных во втором параметре. Во всех примерах в этом руководстве используется версия 1, но при необходимости вы можете обновить базу данных до новой версии. Если указанная версия выше, чем у существующей базы данных, выполняется обратный вызов upgrade в объекте события, что позволяет добавлять в базу данных новые хранилища объектов и индексы.

Объект db в обратном вызове upgrade имеет специальное свойство oldVersion, которое указывает номер версии базы данных, к которой у браузера есть доступ. Этот номер версии можно передать в оператор switch, чтобы выполнить блоки кода в обратном вызове upgrade на основе существующего номера версии базы данных. Пример:

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

В этом примере для последней версии базы данных задается значение 2. При первом выполнении этого кода база данных в браузере ещё не существует, поэтому oldVersion имеет значение 0, а оператор switch начинается с case 0. В примере это добавляет хранилище объектов 'store' в базу данных.

Важное замечание. В операторах switch после каждого блока case обычно используется break, но здесь он намеренно не применяется. Таким образом, если существующая база данных отстает на несколько версий или не существует, код продолжает выполняться через остальные блоки case, пока не станет актуальным. В нашем примере браузер продолжает выполнять код до case 1, создавая индекс name в хранилище объектов store.

Чтобы создать индекс 'description' в хранилище объектов 'store', обновите номер версии и добавьте новый блок case, как показано ниже.

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

Если база данных, созданная в предыдущем примере, все ещё существует в браузере, то при выполнении этого кода oldVersion будет равно 2. Браузер пропускает case 0 и case 1 и выполняет код в case 2, который создает индекс description. После этого в браузере будет база данных версии 3, содержащая хранилище объектов store с индексами name и description.

Дополнительные сведения

Дополнительную информацию об использовании IndexedDB можно найти в следующих ресурсах:

Документация по IndexedDB

Ограничения на хранение данных