使用 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 中的所有读取或写入操作都必须是事务的一部分。这允许原子读取-修改-写入操作,而不会有与其他线程同时作用于数据库的冲突风险。
游标
一种用于迭代数据库中多个记录的机制。

如何检查 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() 方法将新索引的名称作为第一个参数,第二个参数引用您要索引的数据的属性。最后一个参数允许您定义两个选项,这些选项确定索引的运行方式:uniquemultiEntry。如果 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 中的所有数据操作都在事务内部执行。每个操作都有以下形式

  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() 调用都发生在事务中,因此即使 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

  1. 为美味的三明治添加记录。
  2. 为一些鸡蛋添加记录。
  3. 表示事务已完成 (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 循环中,可以读取游标当前位置的行的 keyvalue 属性,您可以以对您的应用程序最有意义的方式对这些值进行操作。当您准备好后,您可以调用 cursor 对象的 continue() 方法以转到下一行,当游标到达数据集的末尾时,while 循环终止。

将游标与范围和索引一起使用

索引允许您按主键以外的属性获取对象存储中的数据。您可以在任何属性上创建索引,该属性将成为索引的 keyPath,在该属性上指定一个范围,并使用 getAll() 或游标获取该范围内的数据。

使用 IDBKeyRange 对象和以下任何方法定义您的范围

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,表示范围内的第一个对象,如果范围内没有数据,则返回 undefinedcursor.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。当此代码首次执行时,浏览器中尚不存在数据库,因此 oldVersion0,并且 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');
    }
  }
});

如果您在上一个示例中创建的数据库仍然存在于浏览器中,则当此代码执行时,oldVersion2。浏览器跳过 case 0case 1,并执行 case 2 中的代码,这将创建一个 description 索引。之后,浏览器将拥有一个版本 3 的数据库,其中包含一个带有 namedescription 索引的 store 对象存储。

延伸阅读

以下资源提供了有关使用 IndexedDB 的更多信息和背景。

IndexedDB 文档

数据存储限制