使用 IndexedDB

IndexedDB 是一种在用户浏览器中持久存储数据的方式。由于它允许您创建具有丰富查询功能的 Web 应用程序,而无论网络可用性如何,因此您的应用程序可以离线和在线工作。

关于本文档

本教程将引导您使用 IndexedDB 的异步 API。如果您不熟悉 IndexedDB,应首先阅读IndexedDB 关键特性和基本术语一文。

有关 IndexedDB API 的参考文档,请参阅IndexedDB API一文及其子页面。本文档记录了 IndexedDB 使用的对象类型,以及异步 API 的方法(同步 API 已从规范中移除)。

基本模式

IndexedDB 鼓励的基本模式如下:

  1. 打开数据库。
  2. 在数据库中创建对象存储。
  3. 启动一个事务并发出一个请求来执行一些数据库操作,例如添加或检索数据。
  4. 通过监听正确类型的 DOM 事件来等待操作完成。
  5. 处理结果(可在请求对象上找到)。

掌握了这些大概念之后,我们可以开始更具体的内容。

创建和结构化存储

打开数据库

我们这样开始整个过程:

js
// Let us open our database
const request = window.indexedDB.open("MyTestDatabase", 3);

看到了吗?打开数据库就像任何其他操作一样——您必须“请求”它。

打开请求不会立即打开数据库或启动事务。对 open() 函数的调用会返回一个 IDBOpenDBRequest 对象,其中包含成功或错误值,您将其作为事件处理。IndexedDB 中的大多数其他异步函数也执行相同的操作——返回一个 IDBRequest 对象,其中包含结果或错误。打开函数的结果是 IDBDatabase 的一个实例。

打开方法的第二个参数是数据库的版本。数据库版本决定了数据库架构——数据库中的对象存储及其结构。如果数据库尚不存在,它将由 open 操作创建,然后触发 onupgradeneeded 事件,您可以在此事件的处理程序中创建数据库架构。如果数据库确实存在,但您指定了升级后的版本号,则会立即触发 onupgradeneeded 事件,允许您在其处理程序中提供更新的架构。稍后在下面的创建或更新数据库版本以及IDBFactory.open参考页面中会详细介绍这一点。

警告:版本号是一个 unsigned long long 数字,这意味着它可能是一个非常大的整数。这也意味着您不能使用浮点数,否则它将被转换为最接近的较小整数,并且事务可能不会启动,也不会触发 upgradeneeded 事件。因此,例如,不要使用 2.4 作为版本号:const request = indexedDB.open("MyTestDatabase", 2.4); // 不要这样做,因为版本将被四舍五入到 2

生成处理程序

您对几乎所有生成的请求都想做的第一件事是添加成功和错误处理程序:

js
request.onerror = (event) => {
  // Do something with request.error!
};
request.onsuccess = (event) => {
  // Do something with request.result!
};

如果请求成功,则触发 success 事件,并调用分配给 onsuccess 的函数。如果请求失败,则触发 error 事件,并调用分配给 onerror 的函数。

IndexedDB API 旨在最大程度地减少错误处理的需求,因此您不太可能看到许多错误事件(至少,一旦您习惯了该 API!)。但是,在打开数据库的情况下,存在一些常见的条件会生成错误事件。最可能的问题是用户决定不授予您的 Web 应用程序创建数据库的权限。IndexedDB 的主要设计目标之一是允许为离线使用存储大量数据。(要了解每个浏览器可以存储多少数据,请参阅浏览器存储配额和逐出标准页面上的“可以存储多少数据?”。)

显然,浏览器不希望允许某些广告网络或恶意网站污染您的计算机,因此浏览器过去在任何给定的 Web 应用程序首次尝试打开 IndexedDB 以进行存储时都会提示用户。用户可以选择允许或拒绝访问。此外,浏览器隐私模式下的 IndexedDB 存储仅在内存中持续到隐身会话关闭。

现在,假设用户允许您创建数据库的请求,并且您收到了一个成功事件来触发成功回调;接下来是什么?这里的请求是通过调用 indexedDB.open() 生成的,因此 request.resultIDBDatabase 的一个实例,您绝对希望将其保存以备后用。您的代码可能看起来像这样:

js
let db;
const request = indexedDB.open("MyTestDatabase");
request.onerror = (event) => {
  console.error("Why didn't you allow my web app to use IndexedDB?!");
};
request.onsuccess = (event) => {
  db = event.target.result;
};

处理错误

如上所述,错误事件会冒泡。错误事件以生成错误的请求为目标,然后事件冒泡到事务,最后冒泡到数据库对象。如果您想避免向每个请求添加错误处理程序,您可以改为在数据库对象上添加一个单一的错误处理程序,如下所示:

js
db.onerror = (event) => {
  // Generic error handler for all errors targeted at this database's
  // requests!
  console.error(`Database error: ${event.target.error?.message}`);
};

打开数据库时常见的可能错误之一是 VER_ERR。它表示存储在磁盘上的数据库版本大于您尝试打开的版本。这是一个必须始终由错误处理程序处理的错误情况。

创建或更新数据库版本

当您创建新数据库或增加现有数据库的版本号(通过指定比以前打开数据库时更高的版本号)时,将触发 onupgradeneeded 事件,并且 IDBVersionChangeEvent 对象将传递给在 request.result 上设置的任何 onversionchange 事件处理程序(即示例中的 db)。在 upgradeneeded 事件的处理程序中,您应该为该版本的数据库创建所需的对象存储:

js
// This event is only implemented in recent browsers
request.onupgradeneeded = (event) => {
  // Save the IDBDatabase interface
  const db = event.target.result;

  // Create an objectStore for this database
  const objectStore = db.createObjectStore("name", { keyPath: "myKey" });
};

在这种情况下,数据库将已经包含来自数据库先前版本的对象存储,因此您不必再次创建这些对象存储。您只需创建任何新的对象存储,或删除先前版本中不再需要的对象存储。如果您需要更改现有对象存储(例如,更改 keyPath),则必须删除旧对象存储并使用新选项再次创建它。(请注意,这将删除对象存储中的信息!如果您需要保存该信息,应在升级数据库之前将其读出并保存到其他位置。)

尝试创建已存在名称的对象存储(或尝试删除不存在名称的对象存储)将抛出错误。

如果 onupgradeneeded 事件成功退出,则打开数据库请求的 onsuccess 处理程序将被触发。

结构化数据库

现在来结构化数据库。IndexedDB 使用对象存储而不是表,单个数据库可以包含任意数量的对象存储。每当值存储在对象存储中时,它都与一个键相关联。根据对象存储是使用键路径还是键生成器,键的提供方式有几种不同的方式。

下表显示了提供键的不同方式:

键路径 (keyPath) 键生成器 (autoIncrement) 描述
此对象存储可以容纳任何类型的值,甚至包括数字和字符串等原始值。每当您想添加新值时,都必须提供一个单独的键参数。
此对象存储只能容纳 JavaScript 对象。对象必须具有与键路径同名的属性。
此对象存储可以容纳任何类型的值。键会自动为您生成,或者如果您想使用特定键,也可以提供一个单独的键参数。
此对象存储只能容纳 JavaScript 对象。通常会生成一个键,并将生成的键的值存储在具有与键路径同名属性的对象中。但是,如果此类属性已经存在,则该属性的值将用作键,而不是生成新键。

您还可以在任何对象存储上创建索引,前提是该对象存储存储的是对象,而不是原始值。索引允许您使用存储对象的属性值(而不是对象的键)来查找对象存储中存储的值。

此外,索引能够对存储的数据强制执行简单的约束。通过在创建索引时设置 unique 标志,索引可确保不会存储两个对象,并且它们具有相同的索引键路径值。因此,例如,如果您有一个对象存储,其中包含一组人员,并且您想确保没有两个人具有相同的电子邮件地址,您可以使用设置了 unique 标志的索引来强制执行此操作。

这听起来可能令人困惑,但这个简单的示例应该能说明这些概念。首先,我们将定义一些客户数据以在示例中使用:

js
// This is what our customer data looks like.
const customerData = [
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" },
];

当然,您不会将某人的社会安全号码用作客户表的主键,因为不是每个人都有社会安全号码,并且您会存储他们的出生日期而不是年龄,但为了方便起见,让我们忽略这些不幸的选择,继续前进。

现在让我们看看如何创建 IndexedDB 来存储我们的数据:

js
const dbName = "the_name";

const request = indexedDB.open(dbName, 2);

request.onerror = (event) => {
  // Handle errors.
};
request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // Create an objectStore to hold information about our customers. We're
  // going to use "ssn" as our key path because it's guaranteed to be
  // unique - or at least that's what I was told during the kickoff meeting.
  const objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // Create an index to search customers by name. We may have duplicates
  // so we can't use a unique index.
  objectStore.createIndex("name", "name", { unique: false });

  // Create an index to search customers by email. We want to ensure that
  // no two customers have the same email, so use a unique index.
  objectStore.createIndex("email", "email", { unique: true });

  // Use transaction oncomplete to make sure the objectStore creation is
  // finished before adding data into it.
  objectStore.transaction.oncomplete = (event) => {
    // Store values in the newly created objectStore.
    const customerObjectStore = db
      .transaction("customers", "readwrite")
      .objectStore("customers");
    customerData.forEach((customer) => {
      customerObjectStore.add(customer);
    });
  };
};

如前所述,onupgradeneeded 是唯一可以更改数据库结构的地方。在此处,您可以创建和删除对象存储,以及构建和删除索引。

对象存储是通过对 createObjectStore() 的一次调用创建的。该方法接受存储的名称和一个参数对象。尽管参数对象是可选的,但它非常重要,因为它允许您定义重要的可选属性并细化要创建的对象存储类型。在我们的例子中,我们请求了一个名为“customers”的对象存储,并定义了一个 keyPath,它是使存储中的单个对象唯一的属性。在此示例中,该属性是“ssn”,因为社会安全号码保证是唯一的。“ssn”必须存在于存储在 objectStore 中的每个对象上。

我们还请求了一个名为“name”的索引,该索引查看存储对象的 name 属性。与 createObjectStore() 一样,createIndex() 接受一个可选的 options 对象,该对象细化要创建的索引类型。添加没有 name 属性的对象仍然会成功,但这些对象不会出现在“name”索引中。

现在我们可以直接从对象存储中通过 ssn 检索存储的客户对象,或者通过使用索引通过其名称检索。要了解如何完成此操作,请参阅使用索引部分。

使用键生成器

在创建对象存储时设置 autoIncrement 标志将为该对象存储启用键生成器。默认情况下,此标志未设置。

使用键生成器,当您将值添加到对象存储时,键将自动生成。键生成器的当前编号在首次创建该键生成器的对象存储时始终设置为 1。基本上,新自动生成的键会根据前一个键增加 1。键生成器的当前编号永远不会减少,除非由于数据库操作被还原而导致,例如,数据库事务被中止。因此,从对象存储中删除记录甚至清除所有记录都不会影响对象存储的键生成器。

我们可以创建另一个带有键生成器的对象存储,如下所示:

js
// Open the indexedDB.
const request = indexedDB.open(dbName, 3);

request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // Create another object store called "names" with the autoIncrement flag set as true.
  const objStore = db.createObjectStore("names", { autoIncrement: true });

  // Because the "names" object store has the key generator, the key for the name value is generated automatically.
  // The added records would be like:
  // key : 1 => value : "Bill"
  // key : 2 => value : "Donna"
  customerData.forEach((customer) => {
    objStore.add(customer.name);
  });
};

有关键生成器的更多详细信息,请参阅规范中的键生成器

添加、检索和删除数据

在对新数据库执行任何操作之前,您需要启动一个事务。事务来自数据库对象,您必须指定事务将跨越哪些对象存储。一旦进入事务,您就可以访问存储数据的对象存储并发出请求。接下来,您需要决定是更改数据库还是仅从中读取。事务有三种可用模式:readonlyreadwriteversionchange

要更改数据库的“架构”或结构(这涉及创建或删除对象存储或索引),事务必须处于 versionchange 模式。此事务通过调用指定了 versionIDBFactory.open 方法打开。

要读取现有对象存储的记录,事务可以是 readonlyreadwrite 模式。要更改现有对象存储,事务必须处于 readwrite 模式。您可以使用 IDBDatabase.transaction 打开此类事务。该方法接受两个参数:storeNames(范围,定义为您要访问的对象存储数组)和事务的 modereadonlyreadwrite)。该方法返回一个包含 IDBIndex.objectStore 方法的事务对象,您可以使用该方法访问您的对象存储。默认情况下,未指定模式时,事务以 readonly 模式打开。

注意:自 Firefox 40 起,IndexedDB 事务放宽了持久性保证以提高性能(参见Firefox bug 1112702)。以前,在 readwrite 事务中,只有当所有数据都保证已刷新到磁盘时,才会触发 complete 事件。在 Firefox 40+ 中,complete 事件在操作系统被告知写入数据之后但在数据实际刷新到磁盘之前触发。因此,complete 事件可能比以前更快地传递,但是,如果操作系统崩溃或在数据刷新到磁盘之前系统断电,则整个事务可能会丢失。由于此类灾难性事件很少发生,因此大多数消费者不应再为此担心。如果您出于某种原因必须确保持久性(例如,您正在存储以后无法重新计算的关键数据),您可以通过使用实验性(非标准)readwriteflush 模式创建事务来强制事务在传递 complete 事件之前刷新到磁盘(参见IDBDatabase.transaction)。

您可以通过在事务中使用正确的范围和模式来加速数据访问。这里有一些提示:

  • 定义范围时,只指定您需要的对象存储。这样,您可以并发运行多个具有不重叠范围的事务。
  • 仅在必要时指定 readwrite 事务模式。您可以并发运行多个具有重叠范围的 readonly 事务,但一个对象存储只能有一个 readwrite 事务。要了解更多信息,请参阅IndexedDB 关键特性和基本术语文章中事务的定义。

向数据库添加数据

如果您刚刚创建了一个数据库,那么您可能想向其中写入数据。下面是它的样子:

js
const transaction = db.transaction(["customers"], "readwrite");
// Note: Older experimental implementations use the deprecated constant IDBTransaction.READ_WRITE instead of "readwrite".
// In case you want to support such an implementation, you can write:
// const transaction = db.transaction(["customers"], IDBTransaction.READ_WRITE);

transaction() 函数接受两个参数(尽管其中一个是可选的)并返回一个事务对象。第一个参数是事务将跨越的对象存储列表。如果您希望事务跨越所有对象存储,则可以传递一个空数组,但不要这样做,因为规范规定空数组应生成 InvalidAccessError。如果您没有为第二个参数指定任何内容,则会获得一个只读事务。由于您想在这里写入数据,因此您需要传递 "readwrite" 标志。

现在您已经有了一个事务,您需要了解它的生命周期。事务与事件循环紧密相关。如果您创建了一个事务并在不使用它的情况下返回到事件循环,那么事务将变为非活动状态。保持事务活动的唯一方法是向它发出请求。当请求完成时,您将获得一个 DOM 事件,并且,假设请求成功,您将在该回调期间有另一个机会来扩展事务。如果您在不扩展事务的情况下返回到事件循环,那么它将变为非活动状态,依此类推。只要有待处理的请求,事务就保持活动状态。事务生命周期实际上非常简单,但可能需要一些时间来适应。更多示例也会有所帮助。如果您开始看到 TRANSACTION_INACTIVE_ERR 错误代码,那么您就出了问题。

事务可以接收三种不同类型的 DOM 事件:errorabortcomplete。我们已经讨论了 error 事件的冒泡方式,因此事务会接收来自它生成的任何请求的错误事件。这里更微妙的一点是,错误的默认行为是中止发生错误的事务。除非您通过首先调用错误事件上的 stopPropagation() 然后执行其他操作来处理错误,否则整个事务将被回滚。这种设计迫使您考虑并处理错误,但如果细粒度错误处理过于繁琐,您始终可以向数据库添加一个包罗万象的错误处理程序。如果您不处理错误事件或在事务上调用 abort(),那么事务将被回滚,并且会在事务上触发 abort 事件。否则,在所有待处理请求完成后,您将获得一个 complete 事件。如果您正在执行大量数据库操作,那么跟踪事务而不是单个请求肯定有助于您的理智。

现在您已经有了一个事务,您需要从中获取对象存储。事务只允许您拥有在创建事务时指定的对象存储。然后您可以添加所需的所有数据。

js
// Do something when all the data is added to the database.
transaction.oncomplete = (event) => {
  console.log("All done!");
};

transaction.onerror = (event) => {
  // Don't forget to handle errors!
};

const objectStore = transaction.objectStore("customers");
customerData.forEach((customer) => {
  const request = objectStore.add(customer);
  request.onsuccess = (event) => {
    // event.target.result === customer.ssn;
  };
});

从调用 add() 生成的请求的 result 是添加的值的键。因此,在这种情况下,它应该等于添加的对象的 ssn 属性,因为对象存储使用 ssn 属性作为键路径。请注意,add() 函数要求数据库中不能已经存在具有相同键的对象。如果您尝试修改现有条目,或者您不关心是否存在,则可以使用 put() 函数,如下面的更新数据库中的条目部分所示。

从数据库中删除数据

删除数据非常相似:

js
const request = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers")
  .delete("444-44-4444");
request.onsuccess = (event) => {
  // It's gone!
};

从数据库中获取数据

现在数据库中包含了一些信息,您可以通过几种方式检索它。首先是简单的 get()。您需要提供键来检索值,如下所示:

js
const transaction = db.transaction(["customers"]);
const objectStore = transaction.objectStore("customers");
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
  // Handle errors!
};
request.onsuccess = (event) => {
  // Do something with the request.result!
  console.log(`Name for SSN 444-44-4444 is ${request.result.name}`);
};

对于“简单”检索来说,这代码量可不少。假设您在数据库级别处理错误,下面是如何缩短它的一些方法:

js
db
  .transaction("customers")
  .objectStore("customers")
  .get("444-44-4444").onsuccess = (event) => {
  console.log(`Name for SSN 444-44-4444 is ${event.target.result.name}`);
};

看到它是如何工作的了吗?由于只有一个对象存储,您可以避免在事务中传递所需对象存储的列表,而只需将名称作为字符串传递。此外,您只是从数据库中读取数据,因此不需要 "readwrite" 事务。调用 transaction() 时不指定模式会为您提供 "readonly" 事务。这里的另一个细微之处是您实际上没有将请求对象保存到变量中。由于 DOM 事件将请求作为其目标,因此您可以使用事件获取 result 属性。

更新数据库中的条目

现在我们已经检索到了一些数据,更新它并将其插入回 IndexedDB 非常简单。让我们对前面的示例进行一些更新:

js
const objectStore = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers");
const request = objectStore.get("444-44-4444");
request.onerror = (event) => {
  // Handle errors!
};
request.onsuccess = (event) => {
  // Get the old value that we want to update
  const data = event.target.result;

  // update the value(s) in the object that you want to change
  data.age = 42;

  // Put this updated object back into the database.
  const requestUpdate = objectStore.put(data);
  requestUpdate.onerror = (event) => {
    // Do something with the error
  };
  requestUpdate.onsuccess = (event) => {
    // Success - the data is updated!
  };
};

因此,在这里我们创建了一个 objectStore 并从中请求一个客户记录,该记录由其 ssn 值(444-44-4444)标识。然后,我们将该请求的结果放入一个变量(data),更新此对象的 age 属性,然后创建第二个请求(requestUpdate)将客户记录放回 objectStore,覆盖之前的值。

注意:在这种情况下,我们必须指定 readwrite 事务,因为我们想要写入数据库,而不仅仅是从中读取。

使用游标

使用 get() 需要您知道要检索哪个键。如果您想遍历对象存储中的所有值,那么您可以使用游标。下面是它的样子:

js
const objectStore = db.transaction("customers").objectStore("customers");

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    console.log(`Name for SSN ${cursor.key} is ${cursor.value.name}`);
    cursor.continue();
  } else {
    console.log("No more entries!");
  }
};

openCursor() 函数接受多个参数。首先,您可以使用我们稍后将介绍的键范围对象来限制检索到的项目范围。其次,您可以指定要迭代的方向。在上面的示例中,我们以升序迭代所有对象。游标的成功回调有点特殊。游标对象本身就是请求的 result(上面我们使用的是简写,所以是 event.target.result)。然后,实际的键和值可以在游标对象的 keyvalue 属性上找到。如果您想继续,则必须在游标上调用 continue()。当您到达数据末尾时(或者如果没有与您的 openCursor() 请求匹配的条目),您仍然会收到成功回调,但 result 属性是 undefined

游标的一个常见模式是检索对象存储中的所有对象并将它们添加到数组中,如下所示:

js
const customers = [];

objectStore.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  } else {
    console.log(`Got all customers: ${customers}`);
  }
};

注意:或者,您可以使用 getAll() 来处理这种情况(以及 getAllKeys())。以下代码与上面执行的操作完全相同:

js
objectStore.getAll().onsuccess = (event) => {
  console.log(`Got all customers: ${event.target.result}`);
};

查看游标的 value 属性会产生性能成本,因为对象是惰性创建的。例如,当您使用 getAll() 时,浏览器必须一次性创建所有对象。如果您只对查看每个键感兴趣,例如,使用游标比使用 getAll() 效率更高。但是,如果您尝试获取对象存储中所有对象的数组,请使用 getAll()

使用索引

使用 SSN 作为键存储客户数据是合乎逻辑的,因为 SSN 唯一标识个人。(这是否是出于隐私的好主意是另一个问题,超出本文范围。)但是,如果您需要按名称查找客户,则需要遍历数据库中的每个 SSN,直到找到正确的 SSN。这种搜索方式会非常慢,因此您可以改为使用索引。

js
// First, make sure you created index in request.onupgradeneeded:
// objectStore.createIndex("name", "name");
// Otherwise you will get DOMException.

const index = objectStore.index("name");

index.get("Donna").onsuccess = (event) => {
  console.log(`Donna's SSN is ${event.target.result.ssn}`);
};

“name”索引不是唯一的,因此可能存在多个 name 设置为 "Donna" 的条目。在这种情况下,您总是会得到键值最低的那个。

如果您需要访问具有给定 name 的所有条目,则可以使用游标。您可以在索引上打开两种不同类型的游标。普通游标将索引属性映射到对象存储中的对象。键游标将索引属性映射到用于将对象存储在对象存储中的键。差异在此处说明:

js
// Using a normal cursor to grab whole customer record objects
index.openCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the whole object.
    console.log(
      `Name: ${cursor.key}, SSN: ${cursor.value.ssn}, email: ${cursor.value.email}`,
    );
    cursor.continue();
  }
};

// Using a key cursor to grab customer record object keys
index.openKeyCursor().onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.primaryKey is the SSN.
    // No way to directly get the rest of the stored object.
    console.log(`Name: ${cursor.key}, SSN: ${cursor.primaryKey}`);
    cursor.continue();
  }
};

索引也可以在多个属性上创建,从而允许使用值的组合来查找记录,例如通过姓名和电子邮件查找一个人。要创建复合索引,请在调用 createIndex 时将属性名称数组作为键路径传递。然后,您可以通过以相同顺序传递值数组来查询索引。

首先,确保您在 request.onupgradeneeded 中创建了索引:

js
const index = objectStore.createIndex("name_email", ["name", "email"]);

然后稍后您可以像这样查询索引:

js
const index = objectStore.index("name_email");

index.get(["Donna", "donna@home.org"]).onsuccess = (event) => {
  console.log(event.target.result);
  // {ssn: '555-55-5555', name: 'Donna', age: 32, email: 'donna@home.org'}
};

指定游标的范围和方向

如果您想限制游标中看到的值的范围,可以使用 IDBKeyRange 对象并将其作为第一个参数传递给 openCursor()openKeyCursor()。您可以创建一个仅允许单个键的键范围,或者一个具有下限或上限的键范围,或者一个同时具有下限和上限的键范围。边界可以是“闭合的”(即,键范围包含给定的值)或“开放的”(即,键范围不包含给定的值)。下面是它的工作原理:

js
// Only match "Donna"
const singleKeyRange = IDBKeyRange.only("Donna");

// Match anything past "Bill", including "Bill"
const lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// Match anything past "Bill", but don't include "Bill"
const lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// Match anything up to, but not including, "Donna"
const upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

// Match anything between "Bill" and "Donna", but not including "Donna"
const boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

// To use one of the key ranges, pass it in as the first argument of openCursor()/openKeyCursor()
index.openCursor(boundKeyRange).onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

有时您可能希望按降序而不是升序迭代(所有游标的默认方向)。切换方向通过将 prev 作为第二个参数传递给 openCursor() 函数来完成:

js
objectStore.openCursor(boundKeyRange, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

如果您只想指定方向更改而不限制显示的结果,则可以仅将 null 作为第一个参数传递:

js
objectStore.openCursor(null, "prev").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

由于“name”索引不是唯一的,因此可能存在多个 name 相同的条目。请注意,这种情况在对象存储中不会发生,因为键必须始终是唯一的。如果您希望在索引游标迭代期间过滤掉重复项,可以将 nextunique(如果您向后遍历,则为 prevunique)作为方向参数传递。当使用 nextuniqueprevunique 时,键值最低的条目始终是返回的条目。

js
index.openKeyCursor(null, "nextunique").onsuccess = (event) => {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

请参阅“IDBCursor 常量”以获取有效的方向参数。

当 Web 应用程序在另一个标签页中打开时版本更改

当您的 Web 应用程序以需要数据库版本更改的方式进行更改时,您需要考虑如果用户在一个标签页中打开旧版本的应用程序,然后在另一个标签页中加载新版本的应用程序会发生什么。当您使用比数据库实际版本更高的版本调用 open() 时,所有其他打开的数据库都必须明确确认请求,然后您才能开始更改数据库(在它们关闭或重新加载之前会触发 onblocked 事件)。下面是它的工作原理:

js
const openReq = mozIndexedDB.open("MyTestDatabase", 2);

openReq.onblocked = (event) => {
  // If some other tab is loaded with the database, then it needs to be closed
  // before we can proceed.
  console.log("Please close all other tabs with this site open!");
};

openReq.onupgradeneeded = (event) => {
  // All other databases have been closed. Set everything up.
  db.createObjectStore(/* … */);
  useDatabase(db);
};

openReq.onsuccess = (event) => {
  const db = event.target.result;
  useDatabase(db);
};

function useDatabase(db) {
  // Make sure to add a handler to be notified if another page requests a version
  // change. We must close the database. This allows the other page to upgrade the database.
  // If you don't do this then the upgrade won't happen until the user closes the tab.
  db.onversionchange = (event) => {
    db.close();
    console.log(
      "A new version of this page is ready. Please reload or close this tab!",
    );
  };

  // Do stuff with the database.
}

您还应该监听 VersionError 错误,以处理已打开的应用程序可能启动导致新尝试打开数据库但使用过时版本的情况。

安全

IndexedDB 使用同源原则,这意味着它将存储绑定到创建它的站点源(通常是站点域或子域),因此其他源无法访问它。

如果浏览器设置为从不接受第三方 Cookie,则第三方窗口内容(例如,<iframe> 内容)无法访问 IndexedDB(请参阅Firefox bug 1147821)。

关于浏览器关闭的警告

当浏览器关闭(因为用户选择了退出选项)、包含数据库的磁盘意外移除或数据库存储权限丢失时,会发生以下情况:

  1. 每个受影响的数据库(或者在浏览器关闭的情况下,所有打开的数据库)上的每个事务都将以 AbortError 终止。效果与在每个事务上调用 IDBTransaction.abort() 相同。
  2. 一旦所有事务完成,数据库连接将关闭。
  3. 最后,表示数据库连接的 IDBDatabase 对象会收到一个 close 事件。您可以使用 IDBDatabase.onclose 事件处理程序来监听这些事件,以便您知道数据库何时意外关闭。

上述行为是新的,仅在以下浏览器版本中可用:Firefox 50、Google Chrome 31(大约)。

在这些浏览器版本之前,事务会静默中止,并且不会触发 close 事件,因此无法检测到意外的数据库关闭。

由于用户可以随时退出浏览器,这意味着您不能依赖任何特定的事务完成,而且在旧版浏览器中,您甚至不会被告知它们何时未完成。这种行为有几个含义。

首先,您应该注意始终在每个事务结束时使数据库保持一致状态。例如,假设您正在使用 IndexedDB 存储允许用户编辑的项目列表。您在编辑后通过清除对象存储然后写入新列表来保存列表。如果您在一个事务中清除对象存储并在另一个事务中写入新列表,则存在浏览器在清除后但在写入前关闭的风险,从而导致数据库为空。为了避免这种情况,您应该将清除和写入合并到一个事务中。

其次,您不应将数据库事务与卸载事件绑定。如果卸载事件是由浏览器关闭触发的,则在卸载事件处理程序中创建的任何事务都将永远不会完成。一种跨浏览器会话维护某些信息的直观方法是在浏览器(或特定页面)打开时从数据库读取它,在用户与浏览器交互时更新它,然后在浏览器(或页面)关闭时将其保存到数据库。但是,这不起作用。数据库事务将在卸载事件处理程序中创建,但由于它们是异步的,因此它们将在执行之前中止。

实际上,无法保证 IndexedDB 事务会完成,即使在正常浏览器关闭时也是如此。请参阅Firefox bug 870645。作为此正常关闭通知的解决方法,您可以跟踪您的事务并添加一个 beforeunload 事件,以在卸载时有任何事务尚未完成时警告用户。

至少随着中止通知和 IDBDatabase.onclose 的添加,您可以知道何时发生了这种情况。

完整的 IndexedDB 示例

我们有一个使用 IndexedDB API 的完整示例。该示例使用 IndexedDB 存储和检索出版物。

另见

如果您想了解更多信息,请进一步阅读。

参考

教程和指南

  • localForage:一个 Polyfill,为客户端数据存储提供简单的名称:值语法,它在后台使用 IndexedDB,但如果浏览器不支持 IndexedDB,则回退到 Web SQL(已弃用),然后是 localStorage。
  • Dexie.js:一个 IndexedDB 包装器,通过简洁、简单的语法实现更快的代码开发。
  • JsStore:一个简单而高级的 IndexedDB 包装器,具有类似 SQL 的语法。
  • MiniMongo:一个客户端内存 MongoDB,由 localStorage 支持,通过 http 与服务器同步。MiniMongo 被 MeteorJS 使用。
  • PouchDB:使用 IndexedDB 在浏览器中实现的 CouchDB 客户端。
  • IDB:一个小型库,主要镜像 IndexedDB API,但具有小的可用性改进。
  • idb-keyval:一个超简单、小巧(~600B)的基于 Promise 的键值存储,使用 IndexedDB 实现。
  • $mol_db:小型(~1.3kB)TypeScript 外观,具有基于 Promise 的 API 和自动迁移。
  • RxDB:一个可以在 IndexedDB 之上使用的 NoSQL 客户端数据库。支持索引、压缩和复制。还为 IndexedDB 添加了跨标签页功能和可观测性。