使用 IndexedDB

IndexedDB 基础知识指南。

本指南介绍了 IndexedDB API 的基础知识。我们使用的是 Jake Archibald 的 IndexedDB promised 库,该库与 IndexedDB API 非常相似,但使用了 promise(您可以 await 来实现更简洁的语法)。这样可以在保持其结构的同时简化 API。

什么是 IndexedDB?

IndexedDB 是一个大型的 NoSQL 存储系统,可以在用户浏览器中存储几乎任何内容。除了常见的搜索、获取和放置操作,IndexedDB 还支持事务。下面是 MDN 上的 IndexedDB 的定义:

IndexedDB 是一个低级 API,用于在客户端存储大量结构化数据(包括文件/blob)。此 API 使用索引实现对此数据的高性能搜索。虽然“DOM 存储”适合存储少量数据,但用于存储大量结构化数据不太实用。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,您可以创建任意名称的多个数据库。如果尝试打开某个数据库时不存在,则会自动创建一个。如需打开数据库,您可以使用 openDB() 方法和 idb 库:

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 数据库包含一个或多个对象存储。对象存储的概念与 SQL 数据库中的表类似。与 SQL 表一样,对象存储包含行和列,但在 IndexedDB 中,如果 IndexedDB 对象存储包含一个键列,以及另一个列用于存储与该键关联的数据,则列数的灵活性较低。

创建对象存储

例如,假设有一个网站保存用户个人资料和记事,您可以想象一个包含 person 对象的 people 对象存储和一个 notes 对象存储。结构合理的 IndexedDB 数据库应该为需要持久保留的每种数据类型提供一个对象存储。

为了确保数据库的完整性,只能通过 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 会创建一个键,并将其与数据分开存储。

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();

此示例会创建一个名为 'notes' 的对象存储,并将主键设置为自动分配为自动递增数字。

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();

此示例与上一个示例类似,但这次会将自动递增值显式分配给名为 'id' 的属性。

选择用来定义键的方法取决于您的数据。如果您的数据具有始终唯一的属性,您可以将其设为 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()

如何使用数据

本部分介绍如何创建、读取、更新和删除数据。这些操作都是异步的,使用 Promise,而 IndexedDB API 使用请求。这样可以简化 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 方法,并且数据会传递给每条记录。Promise.all 调用本身可以执行 await 操作,并在事务完成时完成。

读取数据

如需读取数据,请对您使用 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 in-line 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() 用作事务的一部分,这与使用 add() 方法时非常相似。以下示例使用前面提到的 '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 循环中,可以读取游标当前位置的行的 keyvalue 属性,并且您可以通过对您的应用最有意义的任何方式处理这些值。准备就绪后,您可以调用 cursor 对象的 continue() 方法来转到下一行,到达数据集末尾后,while 循环就会终止。

如何将游标与范围和索引搭配使用

您可以通过几种不同的方式获取所有数据,但如果您只希望基于特定媒体资源的一部分数据,该怎么办?这正是索引的用武之地。借助索引,您可以通过主键以外的属性提取对象存储中的数据。您可以针对任何属性创建索引(该属性将成为索引的 keyPath),在该属性上指定一个范围,并使用 getAll() 方法或游标获取该范围内的数据。

您可以使用 IDBKeyRange 对象定义范围。此对象有五种方法来定义范围限制:

如您所料,upperBound()lowerBound() 方法指定了范围的上限和下限。

IDBKeyRange.lowerBound(indexKey);

或者:

IDBKeyRange.upperBound(indexKey);

它们各自接受一个参数,该参数是您要指定为上限或下限的项的索引的 keyPath 值。

bound() 方法用于指定上限和下限,并将下限作为第一个参数:

IDBKeyRange.bound(lowerIndexKey, upperIndexKey);

这些函数的范围默认包含边界值,但可以通过传递 true 作为第二个参数(如果是 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' 对象存储。

如需在 '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 的数据库,其中包含一个带有 'name''description' 索引的 'store' 对象存储。

深入阅读

关于使用 IndexedDB,以下资源可以提供更多信息和上下文。

IndexedDB 文档

数据存储限制