Spec-Zone.ru › Web APIs

Использование IndexedDB

IndexedDB — это способ сохранения данных в браузере пользователя на постоянной основе. Поскольку он позволяет создавать веб-приложения с богатыми возможностями запросов независимо от доступности сети, ваши приложения могут работать как онлайн, так и офлайн.

О данном документе

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

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

Основной шаблон

Основной шаблон, который поощряет IndexedDB, следующий:

  1. Открыть базу данных.
  2. Создать хранилище объектов в базе данных.
  3. Запустить транзакцию и выполнить запрос для выполнения операции с базой данных, например, добавления или извлечения данных.
  4. Дождаться завершения операции, прослушивая соответствующий тип события DOM.
  5. Выполнить действия с результатами (которые можно найти в объекте запроса).

С этими основными понятиями мы можем перейти к более конкретным аспектам.

Создание и структурирование хранилища

Открытие базы данных

Мы начинаем весь процесс так:

// Let us open our database
const request = window.indexedDB.open("MyTestDatabase", 3);

Видите? Открытие базы данных — это как любая другая операция — вы должны «запросить» её.

Запрос на открытие не открывает базу данных или не запускает транзакцию сразу. Вызов функции open() возвращает объект IDBOpenDBRequest с результатом (успех) или значением ошибки, которые вы обрабатываете как событие. Большинство других асинхронных функций в IndexedDB делают то же самое — возвращают объект IDBRequest с результатом или ошибкой. Результат для функции открытия — экземпляр IDBDatabase.

Вторым параметром метода open является версия базы данных. Версия базы данных определяет схему базы данных — хранилища объектов в базе данных и их структуру. Если база данных ещё не существует, она создаётся операцией open, затем срабатывает событие onupgradeneeded, и вы создаёте схему базы данных в обработчике этого события. Если база данных существует, но вы указываете номер обновлённой версии, сразу срабатывает событие onupgradeneeded, позволяя вам предоставить обновлённую схему в его обработчике. Подробнее об этом позже в разделе Создание или обновление версии базы данных ниже, а также на странице справки IDBFactory.open.

Предупреждение: Номер версии — это unsigned long long число, что означает, что это может быть очень большое целое число. Это также означает, что вы не можете использовать число с плавающей запятой, иначе оно будет преобразовано в ближайшее меньшее целое число, и транзакция может не начаться, ни событие upgradeneeded не сработает. Например, не используйте 2,4 как номер версии: const request = indexedDB.open("MyTestDatabase", 2.4); // don't do this, as the version will be rounded to 2

Генерация обработчиков

Первое, что вам нужно сделать почти со всеми создаваемыми запросами, это добавить обработчики успеха и ошибки:

request.onerror = (event) => {
  // Do something with request.error!
};
request.onsuccess = (event) => {
  // Do something with request.result!
};

Какая из двух функций, onsuccess() или onerror(), вызывается? Если всё прошло успешно, срабатывает событие успеха (то есть событие DOM, у которого свойство type установлено в "success") с request в качестве его target. После срабатывания вызывается функция onsuccess() на request со событием успеха в качестве аргумента. В противном случае, если возникла проблема, срабатывает событие ошибки (то есть событие DOM, у которого свойство type установлено в "error") на request. Это вызывает функцию onerror() с событием ошибки в качестве аргумента.

API IndexedDB разработан для минимизации необходимости обработки ошибок, поэтому вы, скорее всего, не увидите много событий об ошибках (по крайней мере, не после того, как вы привыкнете к API!). Однако при открытии базы данных есть некоторые распространённые условия, которые генерируют события об ошибках. Наиболее вероятная проблема заключается в том, что пользователь отказался предоставить вашему веб-приложению разрешение на создание базы данных. Одна из основных целей проектирования IndexedDB — позволить хранить большие объёмы данных для использования в автономном режиме. (Чтобы узнать больше о том, сколько хранилища доступно для каждого браузера, см. Сколько данных можно сохранить? на странице квоты хранилища браузера и критерии вытеснения.)

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

Теперь, предположим, что пользователь разрешил ваш запрос на создание базы данных, и вы получили событие об успехе для запуска обратного вызова об успехе; Что дальше? Здесь запрос был сгенерирован вызовом indexedDB.open(), поэтому request.result — это экземпляр IDBDatabase, и вы обязательно хотите сохранить его на будущее. Ваш код может выглядеть примерно так:

let db;
const request = indexedDB.open("MyTestDatabase");
request.onerror = (event) => {
  console.error("Why didn't you allow my web app to use IndexedDB?!");
};
request.onsuccess = (event) => {
  db = event.target.result;
};

Обработка ошибок

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

db.onerror = (event) => {
  // Generic error handler for all errors targeted at this database's
  // requests!
  console.error(`Database error: ${event.target.error?.message}`);
};

Одна из распространённых возможных ошибок при открытии базы данных — VER_ERR. Она указывает на то, что версия базы данных, хранящейся на диске, больше, чем версия, которую вы пытаетесь открыть. Это случай ошибки, который всегда должен обрабатываться обработчиком ошибок.

Создание или обновление версии базы данных

Когда вы создаёте новую базу данных или увеличиваете номер версии существующей базы данных (указав больший номер версии, чем вы указали ранее, когда Открытие базы данных), событие onupgradeneeded будет сгенерировано, и объект IDBVersionChangeEvent будет передан любому обработчику события onversionchange , настроенному на request.result (например, db в примере). В обработчике события upgradeneeded вы должны создать хранилища объектов, необходимые для этой версии базы данных:

// This event is only implemented in recent browsers
request.onupgradeneeded = (event) => {
  // Save the IDBDatabase interface
  const db = event.target.result;

  // Create an objectStore for this database
  const objectStore = db.createObjectStore("name", { keyPath: "myKey" });
};

В этом случае база данных уже содержит хранилища объектов из предыдущей версии базы данных, поэтому вам не нужно создавать эти хранилища объектов снова. Вам нужно только создать любые новые хранилища объектов или удалить хранилища объектов из предыдущей версии, которые больше не нужны. Если вам нужно изменить существующее хранилище объектов (например, изменить keyPath), то вы должны удалить старое хранилище объектов и создать его заново с новыми параметрами. (Обратите внимание, что это удалит информацию в хранилище объектов! Если вам нужно сохранить эту информацию, вы должны её прочитать и сохранить где-то ещё перед обновлением базы данных.)

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

Если событие onupgradeneeded завершится успешно, затем будет вызван обработчик onsuccess запроса на открытие базы данных.

Структурирование базы данных

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

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

Путь ключа (keyPath) Генератор ключей (autoIncrement) Описание
Нет Нет Этот магазин объектов может хранить любые значения, даже примитивные, такие как числа и строки. Вы должны предоставить отдельный аргумент ключа всякий раз, когда хотите добавить новое значение.
Да Нет Этот магазин объектов может хранить только JavaScript-объекты. Объекты должны иметь свойство с тем же именем, что и путь ключа.
Нет Да Этот магазин объектов может хранить любые значения. Ключ генерируется автоматически, или вы можете предоставить отдельный аргумент ключа, если хотите использовать определенный ключ.
Да Да Этот магазин объектов может хранить только JavaScript-объекты. Обычно ключ генерируется, а значение сгенерированного ключа хранится в объекте в свойстве с тем же именем, что и путь ключа. Однако, если такое свойство уже существует, значение этого свойства используется в качестве ключа вместо генерации нового ключа.

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

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

Это может показаться запутанным, но этот простой пример проиллюстрирует эти концепции. Сначала мы определим некоторые данные о клиентах, которые будем использовать в нашем примере:

// This is what our customer data looks like.
const customerData = [
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" },
];

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

Теперь давайте посмотрим на создание IndexedDB для хранения наших данных:

const dbName = "the_name";

const request = indexedDB.open(dbName, 2);

request.onerror = (event) => {
  // Handle errors.
};
request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // Create an objectStore to hold information about our customers. We're
  // going to use "ssn" as our key path because it's guaranteed to be
  // unique - or at least that's what I was told during the kickoff meeting.
  const objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // Create an index to search customers by name. We may have duplicates
  // so we can't use a unique index.
  objectStore.createIndex("name", "name", { unique: false });

  // Create an index to search customers by email. We want to ensure that
  // no two customers have the same email, so use a unique index.
  objectStore.createIndex("email", "email", { unique: true });

  // Use transaction oncomplete to make sure the objectStore creation is
  // finished before adding data into it.
  objectStore.transaction.oncomplete = (event) => {
    // Store values in the newly created objectStore.
    const customerObjectStore = db
      .transaction("customers", "readwrite")
      .objectStore("customers");
    customerData.forEach((customer) => {
      customerObjectStore.add(customer);
    });
  };
};

Как указано ранее, onupgradeneeded — единственное место, где вы можете изменить структуру базы данных. В нем вы можете создавать и удалять магазины объектов и создавать и удалять индексы.

Магазины объектов создаются с помощью одного вызова createObjectStore(). Метод принимает имя магазина и объект параметров. Хотя объект параметров является необязательным, он очень важен, потому что позволяет определять важные необязательные свойства и уточнять тип создаваемого магазина объектов. В нашем случае мы запросили магазин объектов с именем «клиенты» и определили keyPath, который делает уникальным отдельный объект в магазине. В этом примере это свойство «ssn», так как номер социального страхования гарантированно уникален. «ssn» должен присутствовать в каждом объекте, хранящемся в objectStore.

Мы также запросили индекс с именем «имя», который обращается к свойству name хранимых объектов. Как и с createObjectStore(), createIndex() принимает необязательный объект options, который уточняет тип создаваемого индекса. Добавление объектов, у которых нет свойства name, все еще выполняется успешно, но объекты не появятся в индексе «имя».

Теперь мы можем извлечь сохраненные объекты клиентов, используя их ssn непосредственно из магазина объектов или их имя, используя индекс. Чтобы узнать, как это делается, см. раздел использование индекса.

Использование генератора ключей

Установление флага autoIncrement при создании магазина объектов включит генератор ключей для этого магазина объектов. По умолчанию этот флаг не установлен.

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

Мы можем создать другой магазин объектов с генератором ключей, как показано ниже:

// Open the indexedDB.
const request = indexedDB.open(dbName, 3);

request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // Create another object store called "names" with the autoIncrement flag set as true.
  const objStore = db.createObjectStore("names", { autoIncrement: true });

  // Because the "names" object store has the key generator, the key for the name value is generated automatically.
  // The added records would be like:
  // key : 1 => value : "Bill"
  // key : 2 => value : "Donna"
  customerData.forEach((customer) => {
    objStore.add(customer.name);
  });
};

Дополнительную информацию о генераторе ключей см. в "W3C Генераторы ключей".

Добавление, извлечение и удаление данных

Прежде чем что-либо делать с вашей новой базой данных, вам необходимо начать транзакцию. Транзакции происходят из объекта базы данных, и вы должны указать, какие магазины объектов должна охватывать транзакция. После того, как вы находитесь внутри транзакции, вы можете получить доступ к магазинам объектов, которые хранят ваши данные, и выполнить свои запросы. Затем вам нужно решить, собираетесь ли вы вносить изменения в базу данных или просто нужно прочитать ее. У транзакций есть три доступных режима: readonly, readwrite, и versionchange.

Чтобы изменить «схему» или структуру базы данных — что включает создание или удаление магазинов объектов или индексов — транзакция должна быть в режиме versionchange. Эта транзакция открывается путем вызова метода IDBFactory.open с указанным version.

Для чтения записей существующего магазина объектов транзакция может быть в режиме readonly или readwrite. Для внесения изменений в существующий магазин объектов транзакция должна быть в режиме readwrite. Такие транзакции открываются с помощью IDBDatabase.transaction. Метод принимает два параметра: storeNames (область действия, определенную как массив магазинов объектов, к которым вы хотите получить доступ) и mode (readonly или readwrite)) для транзакции. Метод возвращает объект транзакции, содержащий метод IDBIndex.objectStore, который вы можете использовать для доступа к вашему магазину объектов. По умолчанию, если режим не указан, транзакции открываются в режиме readonly.

Примечание: По состоянию на Firefox 40, транзакции IndexedDB имеют измененные гарантии долговечности для повышения производительности (см. отчет о проблеме Firefox 1112702). Раньше в транзакции readwrite событие complete срабатывало только тогда, когда все данные гарантированно были выведены на диск. В Firefox 40+ событие complete срабатывает после того, как операционная система получила указание записать данные, но потенциально до того, как эти данные фактически будут выведены на диск. Таким образом, событие complete может быть доставлено быстрее, чем раньше, но существует небольшая вероятность того, что вся транзакция будет потеряна, если операционная система аварийно завершит работу или произойдет отключение питания, прежде чем данные будут выведены на диск. Поскольку такие катастрофические события редки, большинству потребителей не нужно беспокоиться. Если вам необходимо гарантировать долговечность по какой-либо причине (например, вы храните критические данные, которые нельзя вычислить позже), вы можете принудительно выполнить сброс транзакции на диск, прежде чем будет доставлено событие complete создав транзакцию с использованием экспериментального (нестандартного) режима readwriteflush (см. IDBDatabase.transaction).

Вы можете ускорить доступ к данным, используя правильную область действия и режим в транзакции. Вот несколько советов:

  • При определении области действия указывайте только необходимые магазины объектов. Таким образом, вы можете выполнять несколько транзакций с непересекающимися областями действия одновременно.
  • Указывайте режим транзакции readwrite только при необходимости. Вы можете одновременно выполнять несколько транзакций readonly с перекрывающимися областями действия, но у вас может быть только одна транзакция readwrite для магазина объектов. Чтобы узнать больше, см. определение транзакции в статье Основные характеристики и терминология IndexedDB.

Добавление данных в базу данных

Если вы только что создали базу данных, то, вероятно, захотите в неё записать данные. Вот как это выглядит:

const transaction = db.transaction(["customers"], "readwrite");
// Note: Older experimental implementations use the deprecated constant IDBTransaction.READ_WRITE instead of "readwrite".
// In case you want to support such an implementation, you can write:
// const transaction = db.transaction(["customers"], IDBTransaction.READ_WRITE);

Функция transaction() принимает два аргумента (хотя один является необязательным) и возвращает объект транзакции. Первый аргумент — список хранилищ объектов, которые охватывает транзакция. Вы можете передать пустой массив, если хотите, чтобы транзакция охватывала все хранилища объектов, но не делайте этого, так как спецификация гласит, что пустой массив должен генерировать InvalidAccessError. Если вы не укажете ничего для второго аргумента, вы получите транзакцию только для чтения. Так как здесь вы хотите записывать данные, вам нужно передать флаг "readwrite".

Теперь, когда у вас есть транзакция, вам нужно понять её жизненный цикл. Транзакции тесно связаны с циклом событий. Если вы создаёте транзакцию и возвращаетесь в цикл событий, не используя её, то транзакция станет неактивной. Единственный способ сохранить активность транзакции — выполнить запрос к ней. Когда запрос завершится, вы получите событие DOM, и, если предположить, что запрос выполнился успешно, у вас будет ещё одна возможность продлить транзакцию во время этого обратного вызова. Если вы вернётесь в цикл событий, не продлив транзакцию, она станет неактивной и так далее. Пока существуют ожидающие запросы, транзакция остаётся активной. Жизненный цикл транзакций на самом деле очень прост, но может потребоваться некоторое время, чтобы к нему привыкнуть. Несколько дополнительных примеров тоже помогут. Если вы начинаете видеть коды ошибок TRANSACTION_INACTIVE_ERR, значит, вы что-то сделали неправильно.

Транзакции могут получать события DOM трёх разных типов: error, abort, и complete. Мы говорили о том, как события error всплывают, поэтому транзакция получает события об ошибках от любых запросов, которые были сгенерированы из неё. Более тонкий момент здесь заключается в том, что по умолчанию ошибка приводит к прерыванию транзакции, в которой она произошла. Если вы не обработаете ошибку, сначала вызвав stopPropagation() на событии об ошибке, а затем сделав что-то ещё, вся транзакция откатывается. Этот дизайн заставляет вас думать о проблемах и обрабатывать ошибки, но вы всегда можете добавить обработчик ошибок по умолчанию для базы данных, если подробная обработка ошибок слишком сложна. Если вы не обрабатываете событие об ошибке или вызываете abort() на транзакции, то транзакция откатывается, и на транзакции генерируется событие abort. В противном случае, после завершения всех ожидающих запросов, вы получите событие complete. Если вы выполняете множество операций с базой данных, отслеживание транзакции, а не отдельных запросов, может определённо помочь вашему психическому состоянию.

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

// Do something when all the data is added to the database.
transaction.oncomplete = (event) => {
  console.log("All done!");
};

transaction.onerror = (event) => {
  // Don't forget to handle errors!
};

const objectStore = transaction.objectStore("customers");
customerData.forEach((customer) => {
  const request = objectStore.add(customer);
  request.onsuccess = (event) => {
    // event.target.result === customer.ssn;
  };
});

Ключом запроса, сгенерированного в результате вызова add(), является ключ значения, которое было добавлено. Таким образом, в данном случае он должен быть равен свойству ssn объекта, который был добавлен, так как хранилище объектов использует свойство ssn для пути к ключу. Обратите внимание, что функция add() требует, чтобы в базе данных не было уже объекта с тем же ключом. Если вы пытаетесь изменить существующую запись или вам всё равно, существует ли она уже, вы можете использовать функцию put(), как показано ниже в разделе Обновление записи в базе данных.

Удаление данных из базы данных

Удаление данных очень похоже:

const request = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers")
  .delete("444-44-4444");
request.onsuccess = (event) => {
  // It's gone!
};

Получение данных из базы данных

Теперь, когда база данных содержит некоторую информацию, вы можете извлечь её несколькими способами. Сначала, простейший способ get(). Вам нужно указать ключ для извлечения значения, как показано ниже:

const transaction = db.transaction(["customers"]);
const objectStore = transaction.objectStore("customers");
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
  // Handle errors!
};
request.onsuccess = (event) => {
  // Do something with the request.result!
  console.log(`Name for SSN 444-44-4444 is ${request.result.name}`);
};

Это много кода для «простого» извлечения. Вот как можно его немного сократить, предполагая, что вы обрабатываете ошибки на уровне базы данных:

db
  .transaction("customers")
  .objectStore("customers")
  .get("444-44-4444").onsuccess = (event) => {
  console.log(`Name for SSN 444-44-4444 is ${event.target.result.name}`);
};

Видите, как это работает? Поскольку существует только одно хранилище объектов, вы можете избежать передачи списка хранилищ объектов, которые вам нужны в вашей транзакции, и просто передать имя в виде строки. Кроме того, вы читаете только из базы данных, поэтому вам не нужна транзакция "readwrite". Вызов transaction() без указанного режима даёт вам транзакцию "readonly". Ещё одна тонкость заключается в том, что вы на самом деле не сохраняете объект запроса в переменную. Так как событие DOM имеет запрос в качестве своей цели, вы можете использовать событие для доступа к свойству result.

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

Теперь, когда мы извлекли данные, обновление и повторное вставка в IndexedDB довольно просты. Давайте немного обновим предыдущий пример:

const objectStore = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers");
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
  // Handle errors!
};
request.onsuccess = (event) => {
  // Get the old value that we want to update
  const data = event.target.result;

  // update the value(s) in the object that you want to change
  data.age = 42;

  // Put this updated object back into the database.
  const requestUpdate = objectStore.put(data);
  requestUpdate.onerror = (event) => {
    // Do something with the error
  };
  requestUpdate.onsuccess = (event) => {
    // Success - the data is updated!
  };
};

Здесь мы создаём objectStore и запрашиваем запись о клиенте из неё, идентифицированную по её значению ssn (444-44-4444). Затем мы помещаем результат этого запроса в переменную (data), обновляем свойство age этого объекта, а затем создаём второй запрос (requestUpdate), чтобы вернуть запись о клиенте в objectStore, перезаписывая предыдущее значение.

Примечание: В этом случае нам пришлось указать транзакцию readwrite , потому что мы хотим записывать в базу данных, а не только читать из неё.

Использование курсора

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

const objectStore = db.transaction("customers").objectStore("customers");

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    console.log(`Name for SSN ${cursor.key} is ${cursor.value.name}`);
    cursor.continue();
  } else {
    console.log("No more entries!");
  }
};

Функция openCursor() принимает несколько аргументов. Во-первых, вы можете ограничить диапазон элементов, используя объект диапазона ключей, о котором мы поговорим чуть позже. Во-вторых, вы можете указать направление итерации. В приведенном выше примере мы выполняем итерацию по всем объектам в порядке возрастания. Обратный вызов успеха для курсоров немного особенный. Сам объект курсора является result запроса (выше мы используем сокращённую запись, поэтому это event.target.result). Затем фактический ключ и значение можно найти в свойствах key и value объекта курсора. Если вы хотите продолжить, вам нужно вызвать continue() на курсоре. Когда вы достигнете конца данных (или если не было записей, соответствующих вашему запросу openCursor()), вы по-прежнему получаете обратный вызов успеха, но свойство result равно undefined.

Один из распространённых шаблонов с курсорами — извлечь все объекты в хранилище объектов и добавить их в массив, как это:

const customers = [];

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  } else {
    console.log(`Got all customers: ${customers}`);
  }
};

Примечание: В качестве альтернативы вы можете использовать getAll() для обработки этого случая (и getAllKeys()). Следующий код делает ровно то же самое, что и выше:

objectStore.getAll().onsuccess = (event) => {
  console.log(`Got all customers: ${event.target.result}`);
};

Существует стоимость производительности, связанная с просмотром свойства value курсора, потому что объект создаётся лениво. Когда вы используете getAll() , например, браузер должен создать все объекты сразу. Если вас интересуют только ключи, например, использование курсора намного эффективнее, чем использование getAll(). Если же вам нужно получить массив всех объектов в хранилище объектов, используйте getAll().

Использование индекса

Хранение данных о клиентах с использованием SSN в качестве ключа логично, так как SSN однозначно идентифицирует человека. (Вопрос о том, хорошая ли это идея с точки зрения конфиденциальности, является другим и выходит за рамки этой статьи.) Однако, если вам нужно найти клиента по имени, вам придётся перебирать все SSN в базе данных, пока вы не найдёте нужный. Поиск таким образом был бы очень медленным, поэтому вместо этого вы можете использовать индекс.

// First, make sure you created index in request.onupgradeneeded:
// objectStore.createIndex("name", "name");
// Otherwise you will get DOMException.

const index = objectStore.index("name");

index.get("Donna").onsuccess = (event) => {
  console.log(`Donna's SSN is ${event.target.result.ssn}`);
};

Индекс «name» не уникален, поэтому может быть более одной записи с name, установленным на "Donna". В этом случае вы всегда получаете запись с наименьшим значением ключа.

Если вам нужно получить доступ ко всем записям с заданным значением name, вы можете использовать курсор. Вы можете открыть два разных типа курсоров для индексов. Обычный курсор сопоставляет свойство индекса объекту в хранилище объектов. Курсор ключей сопоставляет свойство индекса с ключом, используемым для хранения объекта в хранилище объектов. Различия показаны здесь:

// Using a normal cursor to grab whole customer record objects
index.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the whole object.
    console.log(
      `Name: ${cursor.key}, SSN: ${cursor.value.ssn}, email: ${cursor.value.email}`,
    );
    cursor.continue();
  }
};

// Using a key cursor to grab customer record object keys
index.openKeyCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.primaryKey is the SSN.
    // No way to directly get the rest of the stored object.
    console.log(`Name: ${cursor.key}, SSN: ${cursor.primaryKey}`);
    cursor.continue();
  }
};

Определение диапазона и направления курсоров

Если вы хотите ограничить диапазон значений, которые вы видите в курсоре, вы можете использовать объект IDBKeyRange и передать его в качестве первого аргумента функциям openCursor() или openKeyCursor() . Вы можете создать диапазон ключей, который позволяет только один ключ, или диапазон, имеющий нижнюю или верхнюю границу, или диапазон, имеющий как нижнюю, так и верхнюю границу. Граница может быть «закрытой» (т.е. диапазон ключей включает данное(ые) значение(я)) или «открытой» (т.е. диапазон ключей не включает данное(ые) значение(я)). Вот как это работает:

// Only match "Donna"
const singleKeyRange = IDBKeyRange.only("Donna");

// Match anything past "Bill", including "Bill"
const lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// Match anything past "Bill", but don't include "Bill"
const lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// Match anything up to, but not including, "Donna"
const upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

// Match anything between "Bill" and "Donna", but not including "Donna"
const boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

// To use one of the key ranges, pass it in as the first argument of openCursor()/openKeyCursor()
index.openCursor(boundKeyRange).onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

Иногда вам может потребоваться выполнить итерацию в обратном порядке, а не в прямом (по умолчанию для всех курсоров). Изменение направления достигается путём передачи значения prev функции openCursor() в качестве второго аргумента:

objectStore.openCursor(boundKeyRange, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

Если вы хотите только указать изменение направления, но не ограничивать показываемые результаты, вы можете просто передать null в качестве первого аргумента:

objectStore.openCursor(null, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

Поскольку индекс «name» не уникален, может быть несколько записей, где name одинаковое. Обратите внимание, что такая ситуация не может произойти с хранилищами объектов, так как ключ всегда должен быть уникальным. Если вы хотите отфильтровать дубликаты во время итерации курсора по индексам, вы можете передать nextunique (или prevunique, если вы идёте назад) в качестве параметра направления. Когда используется nextunique или prevunique, запись с наименьшим ключом всегда возвращается.

index.openKeyCursor(null, "nextunique").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

См. "IDBCursor Constants" для допустимых аргументов направления.

Изменение версии при открытом веб-приложении в другой вкладке

Когда ваш веб-приложение изменяется таким образом, что требуется изменение версии базы данных, вы должны учитывать, что произойдет, если пользователь имеет старую версию вашего приложения, открытую в одном окне, а затем загружает новую версию вашего приложения в другом. Когда вы вызываете open() с большей версией, чем фактическая версия базы данных, все другие открытые базы данных должны явно подтвердить запрос, прежде чем вы сможете начать вносить изменения в базу данных (событие onblocked генерируется, пока они не будут закрыты или перезагружены). Вот как это работает:

const openReq = mozIndexedDB.open("MyTestDatabase", 2);

openReq.onblocked = (event) => {
  // If some other tab is loaded with the database, then it needs to be closed
  // before we can proceed.
  console.log("Please close all other tabs with this site open!");
};

openReq.onupgradeneeded = (event) => {
  // All other databases have been closed. Set everything up.
  db.createObjectStore(/* … */);
  useDatabase(db);
};

openReq.onsuccess = (event) => {
  const db = event.target.result;
  useDatabase(db);
  return;
};

function useDatabase(db) {
  // Make sure to add a handler to be notified if another page requests a version
  // change. We must close the database. This allows the other page to upgrade the database.
  // If you don't do this then the upgrade won't happen until the user closes the tab.
  db.onversionchange = (event) => {
    db.close();
    console.log(
      "A new version of this page is ready. Please reload or close this tab!",
    );
  };

  // Do stuff with the database.
}

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

Безопасность

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

Контент стороннего окна (например, <iframe> контент) не может получить доступ к IndexedDB, если в браузере установлен параметр никогда не принимать файлы cookie третьих сторон (см. ошибку Firefox 1147821).

Предупреждение о закрытии браузера

Когда браузер закрывается (потому что пользователь выбрал опцию «Выход» или «Закрыть»), диск с базой данных удаляется неожиданно или теряются разрешения на хранилище базы данных, происходят следующие события:

  1. Каждая транзакция в каждой затронутой базе данных (или во всех открытых базах данных в случае закрытия браузера) прерывается с AbortError. Эффект такой же, как если бы IDBTransaction.abort() был вызван для каждой транзакции.
  2. После завершения всех транзакций соединение с базой данных закрывается.
  3. Наконец, объект IDBDatabase, представляющий соединение с базой данных, получает событие close. Вы можете использовать обработчик событий IDBDatabase.onclose для прослушивания этих событий, чтобы знать, когда база данных неожиданно закрыта.

Указанное выше поведение является новым и доступно только начиная с следующих версий браузеров: Firefox 50, Google Chrome 31 (приблизительно).

До этих версий браузеров транзакции прерываются без уведомления, и событие close не генерируется, поэтому нет способа обнаружить неожиданное закрытие базы данных.

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

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

Во-вторых, никогда не связывайте транзакции базы данных с событиями загрузки. Если событие загрузки запускается при закрытии браузера, любые транзакции, созданные в обработчике событий загрузки, никогда не завершатся. Интуитивный подход к сохранению некоторой информации между сессиями браузера заключается в чтении её из базы данных при открытии браузера (или конкретной страницы), обновлении её по мере взаимодействия пользователя с браузером и сохранении её в базу данных при закрытии браузера (или страницы). Однако это не сработает. Транзакции базы данных будут созданы в обработчике события загрузки, но поскольку они асинхронны, они будут прерваны, прежде чем смогут быть выполнены.

На самом деле, нет гарантии, что транзакции IndexedDB завершатся, даже при нормальном завершении работы браузера. См. ошибку Firefox 870645. В качестве обходного пути для уведомления о нормальном завершении работы вы можете отслеживать свои транзакции и добавлять событие beforeunload, чтобы предупредить пользователя, если какие-либо транзакции ещё не завершены во время разгрузки.

По крайней мере, добавив уведомления о прерывании и IDBDatabase.onclose, вы можете узнать, когда это произошло.

Полный пример IndexedDB

У нас есть полный пример использования API IndexedDB. В примере используется IndexedDB для хранения и извлечения публикаций.

  • Попробовать пример
  • Посмотреть исходный код

См. также

Дополнительные материалы для получения дополнительной информации, если это необходимо.

Справочник

  • Справочник API IndexedDB
  • Спецификация API баз данных IndexedDB
  • Файлы интерфейсов IndexedDB в исходном коде Firefox

Учебники и руководства

  • Связывание элементов пользовательского интерфейса с IndexedDB (2012)
  • IndexedDB — Хранилище в вашем браузере

Библиотеки

  • localForage: Полифил, предоставляющий простой синтаксис для хранения данных на стороне клиента, который использует IndexedDB в фоновом режиме, но переходит к Web SQL (устарело), а затем к localStorage в браузерах, которые не поддерживают IndexedDB.
  • Dexie.js: Обёртка для IndexedDB, которая позволяет значительно ускорить разработку кода за счёт удобного и простого синтаксиса.
  • JsStore: Простая и продвинутая обёртка для IndexedDB, имеющая синтаксис, похожий на SQL.
  • MiniMongo: Клиентский ин-мемори MongoDB, основанный на localstorage с синхронизацией сервера через http. MiniMongo используется MeteorJS.
  • PouchDB: Клиентская реализация CouchDB в браузере, использующая IndexedDB.
  • IDB: Небольшая библиотека, которая в основном отображает API IndexedDB, но с небольшими улучшениями в плане удобства использования.
  • idb-keyval: Крайне простая (~600 байт) promise-based система хранения ключ-значение, реализованная с помощью IndexedDB.
  • $mol_db: Небольшая (~1,3 кб) TypeScript-обёртка с promise-based API и автоматическими миграциями.
  • RxDB: NoSQL база данных на стороне клиента, которая может быть использована поверх IndexedDB. Поддерживает индексы, сжатие и репликацию. Также добавляет кросс-табличные функции и возможности наблюдения для IndexedDB.

© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API