IndexedDB 用法介绍
本指南介绍 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 中的所有读取或写入操作都必须是事务的一部分。这样可以实现原子读取-修改-写入操作,而不会面临与其他同时对数据库执行操作的线程冲突的风险。
- 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'
的数据库。为简单起见,此示例中删除了可选事件对象,但您需要指定该对象,才能使用 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
对象存储。