使用 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 方法的第二个参数是数据库的版本。数据库的版本决定了数据库模式——数据库中的对象存储及其结构。如果数据库尚不存在,则通过 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!
};

onsuccess()onerror() 这两个函数中的哪一个会被调用?如果一切成功,则会触发一个成功事件(即,其 type 属性设置为 "success" 的 DOM 事件),并将 request 作为其 target。触发后,request 上的 onsuccess() 函数将使用成功事件作为其参数被触发。否则,如果出现任何问题,则会在 request 上触发一个错误事件(即,其 type 属性设置为 "error" 的 DOM 事件)。这将触发 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(即示例中的 db)上设置的任何 onversionchange 事件处理程序。在 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 对象。通常会生成一个键,并且生成的键的值存储在具有与键路径相同的名称的属性的对象中。但是,如果此类属性已存在,则该属性的值将用作键,而不是生成新键。

您也可以在任何对象存储上创建索引,前提是对象存储保存的是对象,而不是基本类型。索引允许您使用存储对象的属性值来查找对象存储中存储的值,而不是对象的键。

此外,索引能够对存储的数据实施简单的约束。在创建索引时设置唯一标志,索引可以确保没有两个对象存储都具有索引键路径的相同值。例如,如果您有一个对象存储,其中保存着一组人员,并且您希望确保没有两个人具有相同的电子邮件地址,则可以使用具有唯一标志的索引来强制执行此操作。

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

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

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

现在让我们看看如何创建 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);
  });
};

有关键生成器的更多详细信息,请参阅“W3C 键生成器”

添加、检索和删除数据

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

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

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

注意:从 Firefox 40 开始,IndexedDB 事务已放松了持久性保证以提高性能(请参阅Firefox 错误 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();
  }
};

指定游标的范围和方向

如果要限制在游标中看到的值的范围,可以使用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();
  }
};

有关有效的 direction 参数,请参阅“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);
  return;
};

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 错误 1147821)。

关于浏览器关闭的警告

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

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

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

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

由于用户可以随时退出浏览器,这意味着您不能依赖任何特定事务完成,并且在旧版浏览器中,您甚至不知道它们何时未完成。此行为有几个影响。

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

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

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

至少在添加中止通知和IDBDatabase.onclose之后,您可以知道何时发生这种情况。

完整的 IndexedDB 示例

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

另请参阅

进一步阅读,以便您在需要时查找更多信息。

参考

教程和指南

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