本指南介绍了 IndexedDB API。 我们使用的是 Jake Archibald IndexedDB Promise 库,后者与 IndexedDB API 非常相似,但使用的是 promise, 您可以await
以获得更简洁的语法。这简化了 API 以保持其结构不变。
什么是 IndexedDB?
IndexedDB 是一个大规模的 NoSQL 存储系统,该系统允许仅 用户浏览器中的任何内容除了通常的搜索、get 和 IndexedDB 还支持事务,并且非常适合 存储大量结构化数据。
每个 IndexedDB 数据库都是唯一的源站 (通常为网站域名或子域名),这意味着相应网站无法访问或访问 其他来源。它的数据存储限制 通常很大(如果存在),但是不同的浏览器会处理限制 和数据驱逐不同如需了解更多详情,请参阅深入阅读部分。 。
IndexedDB 术语
- 数据库
- IndexedDB 的最高级别。它包含对象存储,而后者又包含 包含要保留的数据您可以使用 无论您选择什么名称。
- 对象存储
- 用于存储数据的单个存储分区,类似于关系型数据库中的表。 通常,每种类型(不是 JavaScript 数据)都有一个对象存储 特定类型)的数据。与数据库表不同,JavaScript 数据 即存储区中的数据类型不需要保持一致。例如,如果应用 有一个
people
对象存储区,其中包含有关三个人的信息,分别是 用户的年龄属性可以是53
、'twenty-five'
和unknown
。 - 索引
- 一种对象存储,用于整理另一个对象存储(称为 引用对象存储)。索引将使用 按此属性检索对象存储中的记录。例如,如果你 存储人员时,您可能希望稍后按姓名、年龄或 您最爱的动物。
- 操作
- 与数据库的交互。
- 事务
- 一个或一组操作的封装容器,用于确保数据库 完整性。如果事务中的某个操作失败,则所有操作都不是 并且数据库会恢复到事务之前的状态 开始了IndexedDB 中的所有读取或写入操作都必须是事务的一部分。 这允许原子化读取-修改-写入操作,而不会出现冲突的风险 其他线程可同时对数据库执行操作。
- Cursor
- 一种用于迭代数据库中多条记录的机制。
如何检查是否支持 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,您可以使用自己选择的任何名称创建多个数据库。如果 但当您尝试打开某个数据库时却发现它不存在自动创建。 如需打开数据库,请使用 idb
库中的 openDB()
方法:
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'
的数据库。 本例中省略了可选的 event 对象 很简单,但您需要指定它才能使用 IndexedDB 执行任何有意义的操作。
如何使用对象存储
一个 IndexedDB 数据库包含一个或多个对象存储,每个对象存储都有一个 一列用于输入键,在另一列中输入与该键关联的数据。
创建对象存储
结构合理的 IndexedDB 数据库应该为每种类型创建一个对象存储 需要持久保留的数据例如, 个人资料和备注可能具有包含 person
的 people
对象存储 对象以及包含 note
对象的 notes
对象存储区。
为了确保数据库的完整性,您只能在 openDB()
调用中的事件对象。事件对象公开了一个 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()
。
如何使用数据
本部分介绍如何创建、读取、更新和删除数据。这些 操作都是异步的,使用 promise(如果 IndexedDB API 使用 请求。这简化了 API。您无需监听 请求之后,您可以对从 ViewModel 返回的数据库对象调用 .then()
openDB()
方法启动与数据库的交互,或 await
其 创建过程。
IndexedDB 中的所有数据操作都在事务内执行。每个 操作的形式如下:
- 获取数据库对象。
- 在数据库上打开事务。
- 打开事务的对象存储。
- 对对象存储执行操作。
事务可以看作是操作或组的安全封装容器 操作。如果事务中的某个操作失败,则所有 操作会被回滚。事务特定于一个或多个对象存储, 您在打开交易时定义的值。它们可以是只读的,也可以是只读的 和写入。这表示事务中的操作是否读取了 或对数据库进行更改
创建数据
如需创建数据,请调用 add()
方法,并传入要添加的数据。add()
方法的第一个参数是要向其添加数据的对象存储,而 第二个参数是一个对象,其中包含您需要的字段和相关数据。 添加。以下是最简单的示例,其中添加了一行数据:
import {openDB} from 'idb'; async function addItemToStore () { const db = await openDB('example-database', 1); await db.add('storeName', { field: 'data' }); } addItemToStore();
每个 add()
调用都发生在事务内,因此即使 promise 解析 但这并不一定意味着操作有效。为了确保 因此,您需要检查整个 系统已使用 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:
- 正在添加美味三明治记录。
- 正在添加一条鸡蛋记录。
- 表明交易已完成的信号 (
tx.done
)。
由于所有这些操作都基于 promise,因此我们需要等待所有 把它们说完就结束将这些 promise 传递给 Promise.all
这是一种很好的符合人体工程学的方法。Promise.all
接受 promise 并在传递给它的所有 promise 都解析完成后完成。
对于所添加的两条记录,事务实例的 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
操作: 您也可以使用 promise 的 .then()
回调。
以下示例对 'test-db4'
数据库的 get()
方法 'foods'
对象存储,用于按 '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()
方法会返回 promise,因此您可以 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();
与其他方法一样,此方法会返回 promise。您还可以将 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();
此示例对 'foods'
对象存储调用 getAll()
。此操作会返回 '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
属性,以及 您可以采用最适合自己业务的方式 应用。准备就绪后,您就可以调用 cursor
对象的 continue()
。 方法转到下一行,并且 while
循环会在光标悬停时终止 到达数据集的末尾。
将游标与范围和索引搭配使用
借助索引,您可以通过非 主键。您可以针对任何属性创建索引,相应属性会成为 keyPath
。 针对索引指定范围,并获取 使用 getAll()
或游标指定范围。
使用 IDBKeyRange
对象定义范围。以及以下任意一项 方法:
upperBound()
。lowerBound()
。bound()
(两者都有)。only()
。includes()
。
upperBound()
和 lowerBound()
方法指定上限和下限 范围。
IDBKeyRange.lowerBound(indexKey);
或者:
IDBKeyRange.upperBound(indexKey);
它们各自接受一个参数:所需项的索引 keyPath
值 指定为上限或下限
bound()
方法会同时指定上限和下限:
IDBKeyRange.bound(lowerIndexKey, upperIndexKey);
这些函数的范围默认包含在内,这意味着它包括 指定为范围限制的数据。要省去这些值 通过将 true
作为第二个参数传递,将范围指定为独占 lowerBound()
或 upperBound()
,或者作为 bound()
(分别表示下限和上限)。
下一个示例使用 'foods'
对象中 'price'
属性的索引 商店。现在,存储区还附加了一个表单,其中包含两个针对 该范围的上限和下限。使用以下代码查找包含 介于这两个限制之间的价格:
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'
索引可让您按价格搜索内容。
然后,该代码会在索引上打开一个光标并传入范围。光标 返回一个表示范围内第一个对象的 promise,或者,如果 undefined
范围内没有任何数据。cursor.continue()
方法会返回一个 该游标表示下一个对象,然后继续循环,直到 直至达到该范围的结束值。
数据库版本控制
调用 openDB()
方法时,您可以指定数据库版本号 。在本指南的所有示例中, 设置为 1
,但如果需要,您可以将数据库升级到新版本, 以某种方式修改它。如果指定的版本高于 现有数据库,系统会执行事件对象中的 upgrade
回调, 让您可以将新的对象存储和索引添加到数据库中。
upgrade
回调中的 db
对象具有特殊的 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
对象存储。
要在 'store'
对象存储中创建 'description'
索引,请更新 版本号,并添加新的 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 的更多信息和上下文。