本指南涵盖了 IndexedDB API 的基础知识。我们正在使用 Jake Archibald 的 IndexedDB Promised 库,该库与 IndexedDB API 非常相似,但使用 Promise,您可以 await
它以获得更简洁的语法。这简化了 API,同时保持了其结构。
什么是 IndexedDB?
IndexedDB 是一个大规模的 NoSQL 存储系统,允许在用户的浏览器中存储几乎任何内容。除了通常的搜索、获取和放置操作之外,IndexedDB 还支持事务,并且非常适合存储大量结构化数据。
每个 IndexedDB 数据库对于一个 源(通常是站点域名或子域名)都是唯一的,这意味着它不能访问任何其他源或被任何其他源访问。它的 数据存储限制 通常很大(如果存在限制),但不同的浏览器处理限制和数据驱逐的方式不同。有关更多信息,请参阅延伸阅读部分。
IndexedDB 术语
- 数据库
- 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,您可以创建多个具有任意名称的数据库。如果您尝试打开数据库时该数据库不存在,则会自动创建该数据库。要打开数据库,请使用 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'
的数据库。在本例中,为了简单起见,省略了可选的事件对象,但是您需要指定它才能使用 IndexedDB 完成任何有意义的工作。
如何使用对象存储
IndexedDB 数据库包含一个或多个对象存储,每个对象存储都有一列用于键,另一列用于与该键关联的数据。
创建对象存储
结构良好的 IndexedDB 数据库应该为每种需要持久化的数据类型都有一个对象存储。例如,一个持久化用户个人资料和笔记的站点可能有一个包含 person
对象的 people
对象存储,以及一个包含 note
对象的 notes
对象存储。
为了确保数据库的完整性,您只能在 openDB()
调用中的事件对象中创建或删除对象存储。事件对象公开了一个 upgrade()
方法,允许您创建对象存储。在 upgrade()
方法内调用 createObjectStore()
方法来创建对象存储
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 使用请求,而这里使用 Promise。这简化了 API。您可以调用从 openDB()
方法返回的数据库对象上的 .then()
来开始与数据库的交互,或者 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
调用,以便在事务完成时完成。
读取数据
要读取数据,请在您使用 openDB()
方法检索的数据库实例上调用 get()
方法。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'
数据库的 'foods'
对象存储上使用 get()
方法,以按 '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();
数据库交互的结构与其他操作相同。请记住通过在您传递给 Promise.all
的数组中包含 tx.done
方法来检查整个事务是否已完成。
获取所有数据
到目前为止,您只一次检索一个对象。您还可以使用 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
,在 store
对象存储上创建一个 name
索引。
要在 '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 的数据库,其中包含一个带有 name
和 description
索引的 store
对象存储。
延伸阅读
以下资源提供了有关使用 IndexedDB 的更多信息和背景。