使用 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,该 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() 调用中的事件对象内创建或移除对象存储区。events 对象会公开一个 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' 的对象存储区,并在 keyPath 选项中将 email 属性指定为主键。

您也可以使用密钥生成器,例如 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 中的所有数据操作都在事务中执行。每个操作都具有以下形式:

  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 循环中,可以读取光标当前位置所在行的 key 和 value 属性,并且您可以采用对应用最有意义的方式对这些值进行操作。准备就绪后,您可以调用 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,表示范围内的第一个对象;如果范围内没有数据,则返回 undefined。cursor.continue() 方法会返回一个表示下一个对象的游标,并继续循环,直到到达范围末尾。

数据库版本控制

调用 openDB() 方法时,您可以在第二个参数中指定数据库版本号。在本指南的所有示例中,版本都已设置为 1,但如果您需要以某种方式修改数据库,则可以将其升级到新版本。如果指定的版本高于现有数据库的版本,则会执行事件对象中的 upgrade 回调,以便您向数据库添加新的对象存储区和索引。

db 回调中的 upgrade 对象具有一个特殊的 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 的更多信息和背景信息。

IndexedDB 文档

数据存储限制