使用 IndexedDB

IndexedDB 是一种可以让你在用户的浏览器内持久化存储数据的方法。IndexedDB 为生成 Web Application 提供了丰富的查询能力,使我们的应用在在线和离线时都可以正常工作。

关于本文档

本篇教程将教会你如何使用 IndexedDB 的异步 API。如果你对 IndexedDB 还不熟悉,你应该首先阅读有关 IndexedDB 的基本概念

有关 IndexedDB API 的参考手册,请参见 IndexedDB 这篇文章及其子页面,包括 IndexedDB 使用的对象类型,以及同步和异步 API 的方法。

基本模式

IndexedDB 鼓励使用的基本模式如下所示:

  1. 打开数据库并且开始一个事务。
  2. 创建一个 object store。
  3. 构建一个请求来执行一些数据库操作,像增加或提取数据等。
  4. 通过监听正确类型的 DOM 事件以等待操作完成。
  5. 在操作结果上进行一些操作(可以在 request 对象中找到)

生成和构建一个对象存储空间

由于 IndexedDB 本身的规范还在持续演进中,当前的 IndexedDB 的实现还是使用浏览器前缀。在规范更加稳定之前,浏览器厂商对于标准 IndexedDB API 可能都会有不同的实现。但是一旦大家对规范达成共识的话,厂商就会不带前缀标记地进行实现。实际上一些实现已经移除了浏览器前缀:IE 10,Firefox 16 和 Chrome 24。当使用前缀的时候,基于 Gecko 内核的浏览器使用 moz 前缀,基于 WebKit 内核的浏览器会使用 webkit 前缀。

使用体验版本的 IndexedDB

如果你希望在仍旧使用前缀的浏览器中测试你的代码, 可以使用下列代码:  

// In the following line, you should include the prefixes of implementations you want to test.
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
// DON'T use "var indexedDB = ..." if you're not in a function.
// Moreover, you may need references to some window.IDB* objects:
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange
// (Mozilla has never prefixed these objects, so we don't need window.mozIDB*)

要注意的是使用前缀的实现可能会有问题,或者是实现的并不完整,也可能遵循的还是旧版的规范。因此不建议在生产环境中使用。我们更倾向于明确的不支持某一浏览器,而不是声称支持但是实际运行中却出问题:

if (!window.indexedDB) {
    window.alert("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.")
}

打开数据库

我们像下面这样开始整个过程:

// 打开我们的数据库
var request = window.indexedDB.open("MyTestDatabase");

看到了吗? 打开数据库就像任何其他操作一样 — 你必须进行 "request"。

IndexedDB 对象只有一个单一方法,open(), 当这个方法被调用时,打开名为 "MyTestDatabase"的数据库。 所有 IndexedDB 数据库都按照同样的 origin 进行存储, 所以 mozilla.com 可能有一个名为 "binky" 的数据库,同时 mozilla.org 也可以有一个完全不同的名为 "binky" 的数据库。如果该数据库不存在,则会被创建;如果已经存在,则被打开。

open 请求不会立即打开数据库或者开始一个事务。 对 open() 函数的调用会返回一个我们可以作为事件来处理的包含 result(成功的话)或者错误值的 IDBOpenDBRequest 对象。在 IndexedDB 中的大部分异步方法做的都是同样的事情 - 返回一个包含 result 或错误的 IDBRequest 对象。open 函数的结果是一个 IDBDatabase对象的实例。

该 open 方法接受第二个参数,就是数据库的版本号。这样我们就可以更新数据库的 schema ,也就是说如果我们打开的数据库不是我们期望的最新版本的话,我们可以对 object store 进行创建或是删除。在这种情况下,我们实现一个 onupgradeneeded 处理函数,在一个允许操作 object stores 的 versionchange 事务中 - 我们在后面的 更新数据库的版本号中会提到更多有关这方面的内容。使用标明期望的版本号来打开数据库的示例如下所示: 

var request = indexedDB.open("MyTestDatabase", 3);
生成处理函数

几乎所有我们产生的请求我们在处理的时候首先要做的就是添加成功和失败处理函数:

request.onerror = function(event) {
  // Do something with request.errorCode!
};
request.onsuccess = function(event) {
  // Do something with request.result!
};

这两个函数的哪一个,onsuccess() 还是 onerror(),会被调用呢?如果一切顺利的话,一个 success 事件(即一个 type 属性被设置成 "success" 的 DOM 事件)会被触发,使用 request 作为它的 target。 一旦它被触发的话,相关 requestonsuccess() 函数就会被触发,使用 success 事件作为它的参数。 否则,如果不是所有事情都成功的话,一个 error 事件(即 type 属性被设置成 "error" 的 DOM 事件) 会在 request 上被触发。这将会触发使用 error 事件作为参数的 onerror() 方法。

IndexedDB 的 API 被设计来尽可能地减少对错误处理的需求,所以你可能不会看到有很多的错误事件(起码,不会在你已经习惯了这些 API 之后!)。然而在打开数据库的情况下,还是有一些会产生错误事件的常见情况。最有可能出现的问题是用户决定不允许你的 web app 访问以创建一个数据库。IndexedDB 的主要设计目标之一就是允许大量数据可以被存储以供离线使用。(要了解关于针对每个浏览器你可以有多少存储空间的更多内容,请参见 存储限制)。  

显然,浏览器不希望允许某些广告网络或恶意网站来污染你的计算机,所以浏览器会在任意给定的 web app 首次尝试打开一个 IndexedDB 存储时对用户进行提醒。用户可以选择允许访问或者拒绝访问。还有,IndexedDB 在浏览器的隐私模式(Firefox 的 Private Browsing 模式和 Chrome 的 Incognito 模式)下是被完全禁止的。 隐私浏览的全部要点在于不留下任何足迹,所以在这种模式下打开数据库的尝试就失败了。

现在,假设用户已经允许了你的要创建一个数据库的请求,同时你也已经收到了一个来触发 success 回调的 success 事件;然后呢?这里的 request 是通过调用 indexedDB.open() 产生的, 所以 request.result 是一个 IDBDatabase 的实例,而且你肯定希望把它保存下来以供后面使用。你的代码看起来可能像这样:

var db;
var request = indexedDB.open("MyTestDatabase");
request.onerror = function(event) {
  alert("Why didn't you allow my web app to use IndexedDB?!");
};
request.onsuccess = function(event) {
  db = request.result;
};
错误处理

如上文所述,错误事件冒泡出来。错误事件都是针对产生这些错误的请求的,然后事件冒泡到事务,然后最终到达数据库对象。如果你希望避免为所有的请求都增加错误处理程序,你可以替代性的仅对数据库对象添加一个错误处理程序,像这样:

db.onerror = function(event) {
  // Generic error handler for all errors targeted at this database's
  // requests!
  alert("Database error: " + event.target.errorCode);
};

在打开数据库时常见的可能出现的错误之一是 VER_ERR。这表明存储在磁盘上的数据库的版本高于你试图打开的版本。这是一种必须要被错误处理程序处理的一种出错情况。

创建和更新数据库版本号

要更新数据库的 schema,也就是创建或者删除对象存储空间,需要实现 onupgradeneeded 处理程序,这个处理程序将会作为一个允许你处理对象存储空间的 versionchange 事务的一部分被调用。

// 该事件仅在较新的浏览器中被实现
request.onupgradeneeded = function(event) { 
   // 更新对象存储空间和索引 .... 
};

在数据库第一次被打开时或者当指定的版本号高于当前被持久化的数据库的版本号时,这个 versionchange 事务将被创建。

版本号是一个 unsigned long long 数字,这意味着它可以是一个非常大的整数。

这也意味着你不能使用浮点数,否则它会被转换成最接近的较小的整数并且事务可能不会启动,同样 upgradeneeded 事件也不会作为一个结果返回。例如不要使用 2.4 作为版本:

var request = indexedDB.open("MyTestDatabase", 2.4); // don't do this, as the version will be rounded to 2

WebKit 支持当前版本的规范但是只有 Chrome 23 及以上才支持。其他和更旧的实现没有实现当前版本的规范,因此还不支持 indexedDB.open(name, version).onupgradeneeded 签名。有关如何在较旧 Webkit 上升级数据库版本的更多信息,请参见 IDBDatabase 参考文档

构建数据库

现在来构建数据库。IndexedDB 使用对象存储空间而不是表,并且一个单独的数据库可以包含任意数量的对象存储空间。每当一个值被存储进一个对象存储空间时,它会被和一个键相关联。键的提供可以有几种不同的方法,这取决于对象存储空间是使用 key path 还是 key generator

下面的表格显示了几种不同的提供键的方法。 

Key PathKey GeneratorDescription
NoNo这种对象存储空间可以持有任意类型的值,甚至是像数字和字符串这种基本数据类型的值。每当我们想要增加一个新值的时候,必须提供一个单独的键参数。
YesNo这种对象存储空间只能持有 JavaScript 对象。这些对象必须具有一个和 key path 同名的属性。
NoYes这种对象存储空间可以持有任意类型的值。键会为我们自动生成,或者如果你想要使用一个特定键的话你可以提供一个单独的键参数。
YesYes这种对象存储空间只能持有 JavaScript 对象。通常一个键被生成的同时,生成的键的值被存储在对象中的一个和 key path 同名的属性中。然而,如果这样的一个属性已经存在的话,这个属性的值被用作键而不会生成一个新的键。

你也可以使用对象存储空间持有的对象,不是基本数据类型,在任何对象存储空间上创建索引。索引可以让你使用被存储的对象的属性的值来查找存储在对象存储空间的值,而不是用对象的键来查找。

此外,索引具有对存储的数据执行简单限制的能力。通过在创建索引时设置 unique 标记,索引可以确保不会有两个具有同样索引 key path 值的对象被储存。因此,举例来说,如果你有一个用于持有一组 people 的对象存储空间,并且你想要确保不会有两个拥有同样 email 地址的 people,你可以使用一个带有 unique 标识的索引来确保这些。

这听起来可能有点混乱,但下面这个简单的例子应该可以演示这些个概念:

// 我们的客户数据看起来像这样。
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" }
];
const dbName = "the_name";

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

request.onerror = function(event) {
  // 错误处理程序在这里。
};
request.onupgradeneeded = function(event) {
  var db = event.target.result;

  // 创建一个对象存储空间来持有有关我们客户的信息。
  // 我们将使用 "ssn" 作为我们的 key path 因为它保证是唯一的。
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // 创建一个索引来通过 name 搜索客户。
  // 可能会有重复的,因此我们不能使用 unique 索引。
  objectStore.createIndex("name", "name", { unique: false });

  // 创建一个索引来通过 email 搜索客户。
  // 我们希望确保不会有两个客户使用相同的 email 地址,因此我们使用一个 unique 索引。
  objectStore.createIndex("email", "email", { unique: true });

  // 在新创建的对象存储空间中保存值
  for (var i in customerData) {
    objectStore.add(customerData[i]);
  }
};

正如前面提到的,onupgradeneeded 是我们唯一可以修改数据库结构的地方。在这里面,我们可以创建和删除对象存储空间以及构建和删除索引。

对象存储空间仅调用 createObjectStore() 就可以创建。这个方法使用存储空间的名称,和一个对象参数。即便这个参数对象是可选的,它还是非常重要的,因为它可以让你定义重要的可选属性和完善你希望创建的对象存储空间的类型。在我们的示例中,我们请求了一个名为“customers” 的对象存储空间并且定义了一个 使得存储空间中每个单独的对象都是唯一的属性作为 key path。在这个示例中的属性是 “ssn”,因为社会安全号码被确保是唯一的。被存储在对象存储空间中的所有对象都必须存在“ssn”。 

我们也请求了一个名为 “name” 的着眼于存储的对象的 name 属性的索引。如同 createObjectStore()createIndex() 使用了一个完善了我们希望创建的索引类型的可选的 options 对象。添加一个不带 name 属性的对象也会成功,但是这个对象不会出现在 "name" 索引中。

我们现在可以使用存储的用户对象的 ssn 直接从对象存储空间中把它们提取出来,或者通过使用索引来使用他们的 name 进行提取。要了解这些是如何实现的,请参见 使用索引 章节。

增加和删除数据

在你可以对新数据库做任何事情之前,你需要开始一个事务。事务来自于数据库对象,而且你必须指定你想让这个事务跨越哪些对象存储空间。另外,你需要决定你是否将要对数据库进行更改或者你只是需要从它里面进行读取。虽然事务具有三种模式(只读,读写,和版本变更),在可以的情况下你最好还是使用只读事务,因为它们可以并发运行。

向数据库中增加数据

如果你刚刚创建了一个数据库,你可能想往里面写点东西。看起来会像下面这样:

var transaction = db.transaction(["customers"], "readwrite");
// 注意: 旧版实验性的实现使用不建议使用的常量 IDBTransaction.READ_WRITE 而不是 "readwrite"。
// 如果你想支持这样的实现,你只要这样写就可以了: 
// var transaction = db.transaction(["customers"], IDBTransaction.READ_WRITE);

transaction() 方法接受三个参数(虽然两个是可选的)并返回一个事务对象。第一个参数是事务希望跨越的对象存储空间的列表。如果你希望事务能够跨越所有的对象存储空间你可以传入一个空数组。如果你没有为第二个参数指定任何内容,你得到的是只读事务。因为这里我们是想要写入所以我们需要传入 "readwrite" 标识。

现在我们已经有了一个事务,我们需要理解它的生命周期。事务和事件循环的联系非常密切。如果你创建了一个事务但是并没有使用它就返回给事件循环,那么事务将变得无效。保持事务活跃的唯一方法就是在其上构建一个请求。当请求完成时你将会得到一个 DOM 事件,并且,假设请求成功了,你将会有另外一个机会在回调中来延长这个事务。如果你没有延长事务就返回到了事件循环,那么事务将会变得不活跃,依此类推。只要还有待处理的请求事务就会保持活跃。事务生命周期真的很简单但是可能需要一点时间你才能对它变得习惯。还有就是来几个例子也会有所帮助。如果你开始看到 TRANSACTION_INACTIVE_ERR 错误代码,那么你已经把某些事情搞乱了。

事务可以接收三种不同类型的 DOM 事件: errorabort,以及 complete。我们已经讨论过 error事件冒泡,所以一个事务要接收所有可能产生错误事件的请求所产生的错误事件。更微妙的一点是一个 error 的默认行为是终止发生错误的事务。除非你在 error 事件上通过调用 preventDefault() 处理了这个错误,整个事务被回滚了。这样的设计迫使你去思考和处理错误,但是如果细粒度的错误处理太过繁琐的话,你也可以总是对数据库添加一个总的错误处理程序。如果你不处理一个错误事件或者你在事务中调用 abort(),那么事务被回滚并且有关事物的一个 abort 事件被触发。否则,在所有的未处理请求都完成后,你将得到一个 complete 事件。如果你正在做大量的数据库操作,那么追踪事务而不是单个的请求当然可以帮助你进行决断。

现在你有了一个事务了,你将需要从它拿到一个对象存储空间。事务只能让你拿到一个你在创建事务时已经指定过的对象存储空间。然后你可以增加所有你需要的数据。

现在你有了一个事务了,你将需要从它拿到一个对象存储空间。事务只能让你拿到一个你在创建事务时已经指定过的对象存储空间。然后你可以增加你需要的所有数据。

// 当所有的数据都被增加到数据库时执行一些操作
transaction.oncomplete = function(event) {
  alert("All done!");
};

transaction.onerror = function(event) {
  // 不要忘记进行错误处理!
};

var objectStore = transaction.objectStore("customers");
for (var i in customerData) {
  var request = objectStore.add(customerData[i]);
  request.onsuccess = function(event) {
    // event.target.result == customerData[i].ssn
  };
}

产生自 add() 调用的请求的 result 是被添加的值的键。因此在这种情况下,它应该等于被添加的对象的 ssn 属性, 因为对象存储空间使用 ssn 属性作为 key path。 注意 add() 函数要求数据库中不能已经有相同键的对象存在。如果你正在试图修改一个现有条目,或者你并不关心是否有一个同样的条目已经存在,使用 put()函数。

从数据库中删除数据

删除数据是非常类似的:

var request = db.transaction(["customers"], "readwrite")
                .objectStore("customers")
                .delete("444-44-4444");
request.onsuccess = function(event) {
  // 删除数据成功!
};

从数据库中获取数据

现在数据库里已经有了一些信息,你可以通过几种方法对它进行提取。首先是简单的 get()。你需要提供键来提取值,像这样:

var transaction = db.transaction(["customers"]);
var objectStore = transaction.objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function(event) {
  // 错误处理!
};
request.onsuccess = function(event) {
  // 对 request.result 做些操作!
  alert("Name for SSN 444-44-4444 is " + request.result.name);
};

对于一个“简单”的提取这里的代码有点多了。下面看我们怎么把它再缩短一点,假设你在数据库的级别上来进行的错误处理:

db.transaction("customers").objectStore("customers").get("444-44-4444").onsuccess = function(event) {
  alert("Name for SSN 444-44-4444 is " + event.target.result.name);
};

这是如何工作的呢?由于只有一个对象存储空间,你可以避免传入一个在你的事务中需要的对象存储空间的列表,而只是作为一个字符串把名字传入即可。同样,你只是在从数据库读取数据,所以你不需要一个 "readwrite"事务。调用一个没有指定模式的 transaction()将给你一个 "readonly"事务。这里的另外一个微妙之处在于你实际上不需要保存请求对象到一个变量。因为 DOM 事件把这个请求作为它的 target,你可以使用 event 来得到 result 属性。很简单,对吧?!

使用游标

使用 get() 要求你知道你想要检索哪一个键。如果你想要遍历对象存储空间中的所有值,那么你可以使用游标。看起来会像下面这样:

var objectStore = db.transaction("customers").objectStore("customers");

objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
    cursor.continue();
  }
  else {
    alert("No more entries!");
  }
};

openCursor() 函数需要几个参数。首先,你可以使用一个 key range 对象来限制被检索的项目的范围。第二,你可以指定你希望进行迭代的方向。在上面的示例中,我们在以升序迭代所有的对象。游标成功的回调有点特别。游标对象本身是请求的 result (上面我们使用的是简写形式,所以是 event.target.result)。然后实际的 key 和 value 可以根据游标对象的 keyvalue 属性被找到。如果你想要保持继续前行,那么你必须调用游标上的 continue() 。当你已经到达数据的末尾时(或者没有匹配 openCursor() 请求的条目)你仍然会得到一个成功回调,但是 result 属性是 undefined。

使用游标的一种常见模式是提取出在一个对象存储空间中的所有对象然后把它们添加到一个数组中,像这样:

var customers = [];

objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  }
  else {
    alert("Got all customers: " + customers);
  }
};
警告: 下面的函数并不是 IndexedDB 标准的一部分!

Mozilla 也已经实现了 getAll()来处理这种情况。它不是 IndexedDB 标准的一部分,所以它未来可能会消失。我们已经把它包含在这里是因为我们觉得它比较有用。下面的代码实现的是跟上面同样的事情:

objectStore.getAll().onsuccess = function(event) {
  alert("Got all customers: " + event.target.result);
};

查找游标的 value属性会引起相关的性能损耗,因为对象是被延迟创建的。当使用 getAll() 时,Gecko 必须立即创建所有的对象。如果你仅是对检索每个键感兴趣,举个例子,使用游标比使用 getAll()要高效的多。如果你试图获得一个对象存储空间内所有对象的一个数组,那么,使用 getAll()

使用索引

使用 SSN 作为键来存储客户数据是合理的,因为 SSN 唯一地标识了一个个体(对隐私来说这是否是一个好的想法是另外一个话题,不在本文的讨论范围内)。如果你想要通过姓名来查找一个客户,那么,你将需要在数据库中迭代所有的 SSN 直到你找到正确的那个。以这种方式来查找将会非常的慢,相反你可以使用索引。

var index = objectStore.index("name");
index.get("Donna").onsuccess = function(event) {
  alert("Donna's SSN is " + event.target.result.ssn);
};

“name” 游标不是唯一的,因此 name 被设成 "Donna" 的记录可能不止一条。在这种情况下,你总是得到键值最小的那个。

如果你需要访问带有给定 name 的所有的记录你可以使用一个游标。你可以在索引上打开两个不同类型的游标。一个常规游标映射索引属性到对象存储空间中的对象。一个键索引映射索引属性到用来存储对象存储空间中的对象的键。不同之处被展示如下:

index.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key 是一个 name, 就像 "Bill", 然后 cursor.value 是整个对象。
    alert("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ", email: " + cursor.value.email);
    cursor.continue();
  }
};

index.openKeyCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is 一个 name, 就像 "Bill", 然后 cursor.value 是那个 SSN。
    // 没有办法可以得到存储对象的其余部分。
    alert("Name: " + cursor.key + ", "SSN: " + cursor.value);
    cursor.continue();
  }
};

指定游标的范围和方向

如果你想要限定你在游标中看到的值的范围,你可以使用一个 key range 对象然后把它作为第一个参数传给 openCursor() 或是 openKeyCursor()。你可以构造一个只允许一个单一 key 的 key range,或者一个具有下限或上限,或者一个既有上限也有下限。边界可以是闭合的(也就是说 key range 包含给定的值)或者是“开放的”(也就是说 key range 不包括给定的值)。这里是它如何工作的:

// 只匹配 "Donna"
var singleKeyRange = IDBKeyRange.only("Donna");

// 匹配所有在 "Bill" 前面的, 包括 "Bill"
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// 匹配所有在 “Bill” 前面的, 但是不需要包括 "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

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

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

index.openCursor(boundKeyRange).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

有时候你可能想要以倒序而不是正序(所有游标的默认顺序)来遍历。切换方向是通过传递 prevopenCursor() 方法来实现的:

objectStore.openCursor(null, IDBCursor.prev).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

因为 “name” 索引不是唯一的,那就有可能存在具有相同 name 的多条记录。要注意的是这种情况不可能发生在对象存储空间上,因为键必须永远是唯一的。如果你想要在游标在索引迭代过程中过滤出重复的,你可以传递 nextunique (或 prevunique 如果你正在向后寻找)作为方向参数。 当 nextunique 或是 prevunique 被使用时,被返回的那个总是键最小的记录。

index.openKeyCursor(null, IDBCursor.nextunique).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

当一个 web app 在另一个标签页中被打开时的版本变更

当你的 web app 在这样一种方式下改变你的数据库时碰到被要求进行版本变化,你需要考虑如果用户已经在一个标签页中打开了你的应用的旧版本的数据库,然后他又在另一个标签页中加载了你的应用的新版本,这种情况下会发生什么事情。当你带着比数据库实际版本更高的版本号调用 open() 时,所有其他打开的数据库必须在你开始实际对数据库进行修改之前显式通知这个请求。这里是它如何工作的:

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

openReq.onblocked = function(event) {
  // 如果其他标签页已经加载了这个数据库,那么
  // 在我们可以继续处理之前它需要被关闭。
  alert("Please close all other tabs with this site open!");
};
  
openReq.onupgradeneeded = function(event) {
  // 所有其它数据库都已经被关掉了。Set everything up.
  db.createObjectStore(/* ... */);
  useDatabase(db);
}  
  
openReq.onsuccess = function(event) {
  var db = event.target.result;
  useDatabase(db);
  return;
}

function useDatabase(db) {
  // 确保添加一个如果另一个页面请求一个版本变化时来被通知的处理程序。
  // 我们必须关闭这个数据库。这就允许其他页面对数据库进行升级。
  // 如果你不这么做的话,除非用户关闭标签页否则升级就不会发生。
  db.onversionchange = function(event) {
    db.close();
    alert("A new version of this page is ready. Please reload!");
  };

  // 其他针对数据库的处理
}

安全

IndexedDB 使用同源原则,这意味着它把存储空间绑定到了创建它的站点的源(典型情况下,就是站点的域或是子域),所以它不能被任何其他源访问。

要着重指出的一点是 IndexedDB 不适用于从另一个站点加载进框架的内容 (不管是 {{ HTMLElement("frame") }} 还是 {{ HTMLElement("iframe") }}。这是一项安全措施。为什么说这一点是很重要的,请查阅 {{ bug(595307) }}.

一个完整的 IndexedDB 示例

HTML 内容

    <h1>IndexedDB Demo: storing blobs, e-publications example</h1>
    <div id="msg">
    </div>

    <form id="register-form">
      <table>
        <tbody>
          <tr>
            <td>
              <label for="pub-title" class="required">
                Title:
              </label>
            </td>
            <td>
              <input type="text" id="pub-title" name="pub-title" />
            </td>
          </tr>
          <tr>
            <td>
              <label for="pub-year" class="required">
                Year:
              </label>
            </td>
            <td>
              <input type="number" id="pub-year" name="pub-year" />
            </td>
          </tr>
          <tr>
            <td>
              <label for="pub-biblioid" class="required">
                Bibliographic ID
                <span class="note">(ISBN, ISSN, etc.)</span>:
              </label>
            </td>
            <td>
              <input type="text" id="pub-biblioid" name="pub-biblioid"/>
            </td>
          </tr>
        </tbody>
        <tbody>
          <tr>
            <td>
              <label for="pub-file">
                File image:
              </label>
            </td>
            <td>
              <input type="file" id="pub-file" accept="image/*"/>
            </td>
          </tr>
          <tr>
            <td>
              <label for="pub-content-url">
                Online-file image URL:
              </label>
            </td>
            <td>
              <input type="text" id="pub-content-url" name="pub-content-url"/>
            </td>
          </tr>
        </tbody>
      </table>

      <div class="button-pane">
        <input type="button" id="add-button" value="Add Publication" />
        <input type="reset" id="register-form-reset"/>
        <span id="action-status"></span>
      </div>
    </form>

    <form id="delete-form">
      <div>
        <label for="pub-biblioid-to-delete" class="required">
          Key (for example 1, 2, 3, etc.):
        </label>
        <input type="text" id="pub-biblioid-to-delete"
               name="pub-biblioid-to-delete" />
      </div>
      <div class="button-pane">
        <input type="button" id="delete-button" value="Delete Publication" />
      </div>
    </form>

    <form id="search-form">
      <div class="button-pane">
        <input type="button" id="search-list-button"
               value="List database content" />
      </div>
    </form>

    <div>
      <div id="pub-msg">
      </div>
      <ul id="pub-list">
      </ul>
    </div>

CSS 内容

body {
  font-size: 0.8em;
  font-family: Sans-Serif;
}

form {
  background-color: #cccccc;
  border-radius: 0.3em;
  display: inline-block;
  margin-bottom: 0.5em;
  padding: 1em;
}

table {
  border-collapse: collapse;
}

input {
  padding: 0.3em;
  border-color: #cccccc;
  border-radius: 0.3em;
}

.required:after {
  content: "*";
  color: red;
}

.button-pane {
  margin-top: 1em;
}

#documents-list {
  background-color: #eeeeee;
}

.action-success {
  margin-left: 1em;
  padding: 0.5em;
  color: #00d21e;
  background-color: #eeeeee;
  border-radius: 0.2em;
}

.action-failure {
  margin-left: 1em;
  padding: 0.5em;
  color: #ff1408;
  background-color: #eeeeee;
  border-radius: 0.2em;
}

.presence-no {
  font-style: italic;
}

.note {
  font-size: smaller;
}

JavaScript 内容

(function () {
  // Works with:
  // * Firefox >= 16.0
  // * Google Chrome >= 24.0 (you may need to get Google Chrome Canary)

  const DB_NAME = 'mdn-demo-indexeddb-epublications';
  const DB_VERSION = 1; // Use a long long for this value (don't use a float)
  const DB_STORE_NAME = 'publications';

  var db;

  function initDb() {
    console.debug("initDb ...");
    var req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onsuccess = function (evt) {
      // Better use "this" than "req" to get the result to avoid problems with
      // garbage collection.
      // db = req.result;
      db = this.result;
      console.debug("initDb DONE");
    };
    req.onerror = function (evt) {
      console.error("initDb:", evt.target.errorCode);
    };

    req.onupgradeneeded = function (evt) {
      console.debug("initDb.onupgradeneeded");
      var store = evt.currentTarget.result.createObjectStore(
        DB_STORE_NAME, { keyPath: 'id', autoIncrement: true });

      store.createIndex('biblioid', 'biblioid', { unique: true });
      store.createIndex('title', 'title', { unique: false });
      store.createIndex('year', 'year', { unique: false });
    };
  }

  function getFile(key, success_callback) {
    var tx = db.transaction(DB_STORE_NAME, 'readonly');
    var store = tx.objectStore(DB_STORE_NAME);
    var req = store.get(key);
    req.onsuccess = function(evt) {
      var value = evt.target.result;
      if (value)
        success_callback(value.file);
    };
  }

  function displayPubList() {
    console.debug("displayPubList");
    var pub_msg = $('#pub-msg');
    pub_msg.empty();
    var pub_list = $('#pub-list');
    pub_list.empty();

    var tx = db.transaction(DB_STORE_NAME, 'readonly');
    var store = tx.objectStore(DB_STORE_NAME);
    var req;

    req = store.count();
    // Requests are executed in the order in which they were made against the
    // transaction, and their results are returned in the same order.
    // Thus the count text below will be displayed before the actual pub list
    // (not that it is algorithmically important in this case).
    req.onsuccess = function(evt) {
      pub_msg.append('<p>There are <strong>' + evt.target.result +
                     '</strong> record(s) in the object store.</p>');
    };
    req.onerror = function(evt) {
      console.error("add error", this.error);
      displayActionFailure(this.error);
    };

    var i = 0;
    var img_id;
    var file_presence;
    var presence_html;
    req = store.openCursor();
    req.onsuccess = function(evt) {
      var cursor = evt.target.result;
      if (cursor) {
        presence_html = "<span class='presence-no'>No image</span>";
        file_presence = cursor.value.file != null;
        console.debug("cursor.value:", cursor.value);
        if (file_presence) {
          img_id = 'pub-img-' + i;
          presence_html = '<img id="' + img_id + '"/>';
          getFile(cursor.key, function(file) {
            console.debug("file:", file);

            // Note that here it is not possible to set a link to the file to
            // make  it possible to download it.
            // The only possible options are:
            // * display the file if it is an image
            // * getting text/other info from the file and display them
            var obj_url = window.URL.createObjectURL(file);
            $('#' + img_id).attr('src', obj_url);
            window.URL.revokeObjectURL(obj_url);
          });
        }
        pub_list.append('<li>' +
                        '[' + cursor.key + '] ' +
                        '(biblioid: ' + cursor.value.biblioid + ') ' +
                        cursor.value.title + ' - ' +
                        cursor.value.year + ' / ' +
                        presence_html +
                        '</li>');

        // Move on to the next object in store
        cursor.continue();

        // This counter serves only to create distinct img ids
        i++;
      } else {
        console.debug("No more entries!");
      }
    };
  };

  function addPublication(biblioid, title, year, file) {
    console.debug("addPublication arguments:", arguments);
    if (!db) {
      console.error("addPublication: the db is not initialized");
      return;
    }
    var tx = db.transaction(DB_STORE_NAME, 'readwrite');
    var store = tx.objectStore(DB_STORE_NAME);
    var req = store.add({ biblioid: biblioid, title: title, year: year,
                          file: file });
    req.onsuccess = function (evt) {
      console.debug("Insertion in DB successful");
      displayPubList();
    };
    req.onerror = function() {
      console.error("add error", this.error);
      displayActionFailure(this.error);
    };
  }

  function displayActionSuccess(msg) {
    msg = typeof msg !== 'undefined' ? "Success: " + msg : "Success";
    $('#action-status').html('<span class="action-success">' + msg + '</span>');
  }
  function displayActionFailure(msg) {
    msg = typeof msg !== 'undefined' ? "Failure: " + msg : "Failure";
    $('#action-status').html('<span class="action-failure">' + msg + '</span>');
  }
  function resetActionStatus() {
    console.debug("resetActionStatus ...");
    $('#action-status').empty();
    console.debug("resetActionStatus DONE");
  }

  function addEventListeners() {
    console.debug("addEventListeners");
    initDb();

    $('#register-form-reset').click(function(evt) {
      resetActionStatus();
    });

    $('#add-button').click(function(evt) {
      console.debug("add ...");
      var title = $('#pub-title').val();
      var year = $('#pub-year').val();
      var biblioid = $('#pub-biblioid').val();
      if (!title || !year || !biblioid) {
        displayActionFailure("Required field(s) missing");
        return;
      }

      var file_input = $('#pub-file');
      var selected_file = file_input.get(0).files[0];
      console.debug("selected_file:", selected_file);
      file_input.val(null);
      var content_url = $('#pub-content-url').val();
      if (selected_file) {
        addPublication(biblioid, title, year, selected_file);
      } else {
        addPublication(biblioid, title, year);
        displayActionSuccess();
      }

    });

    $('#delete-button').click(function(evt) {
      console.debug("delete ...");
      var k = $('#pub-biblioid-to-delete').val();
      console.debug("delete k:", k);
      var tx = db.transaction(DB_STORE_NAME, 'readwrite');
      var store = tx.objectStore(DB_STORE_NAME);

      // Warning: The exact same key used for creation needs to be passed for
      // the deletion. If the key was a Number for creation, then it needs to be
      // a Number for deletion.
      k = Number(k);

      // The code that could be nice if it worked
      // var req = store.delete(k);
      // req.onsuccess = function(evt) {
      //   var record = evt.target.result;
      //   console.debug("record:", record);
      //   if (typeof record !== 'undefined') {
      //     displayActionSuccess("Deletion successful");
      //     displayPubList();
      //   } else {
      //     displayActionFailure("No matching record found");
      //   }
      // };
      // req.onerror = function (evt) {
      //   console.error("delete:", evt.target.errorCode);
      // };

      // The code that actually works
      //
      // As per spec http://www.w3.org/TR/IndexedDB/#object-store-deletion-operation
      // the result of the Object Store Deletion Operation algorithm is
      // undefined, so it's not possible to know if some records were actually
      // deleted by looking at the request result.
      var req = store.get(k);
      req.onsuccess = function(evt) {
        var record = evt.target.result;
        console.debug("record:", record);
        if (typeof record !== 'undefined') {
          req = store.delete(k);
          req.onsuccess = function(evt) {
            console.debug("evt:", evt);
            console.debug("evt.target:", evt.target);
            console.debug("evt.target.result:", evt.target.result);
            console.debug("delete successful");
            displayActionSuccess("Deletion successful");
            displayPubList();
          };
          req.onerror = function (evt) {
            console.error("delete:", evt.target.errorCode);
          };
        } else {
          displayActionFailure("No matching record found");
        }
      };
      req.onerror = function (evt) {
        console.error("delete:", evt.target.errorCode);
      };

    });

    var search_button = $('#search-list-button');
    search_button.click(function(evt) {
      displayPubList();
    });

  }

// function dbAddAndGetBlob(title, year, url) {
//   // Create XHR
//   var xhr = new XMLHttpRequest();
//   var blob;

//   // We can't use jQuery here because as of jQuery 1.7.2 the new "blob"
//   // responseType is not handled.
//   // http://bugs.jquery.com/ticket/7248
//   //
//   // Attention Same origin policy, the URL must come from the same origin than
//   // the web app.
//   xhr.open('GET', url, true);
//   // Set the responseType to blob
//   xhr.responseType = 'blob';
//   xhr.addEventListener("load", function () {
//                          if (xhr.status === 200) {
//                            console.debug("File retrieved");

//                            // Blob as response
//                            blob = xhr.response;
//                            console.debug("Blob:" + blob);

//                            // Insert the received blob into IndexedDB
//                            dbAdd(title, year, blob);
//                          } else {
//                            console.error("loadConfigFormHtmlContent error:",
//                                          xhr.responseText, xhr.status);
//                          }
//                        }, false);
//   // Send XHR
//   xhr.send();
// }

  addEventListeners();
})(); // Immediately-Invoked Function Expression (IIFE)

{{ EmbedLiveSample('Full_IndexedDB_example', 800, 400) }}

下一步

如果你想要多试试这些 API 的用法,请跳转到 参考手册 以查看其他不同的用法。

另请参阅

参考

教程

相关文章

Firefox

本文由范圣刚翻译自 MDN -> IndexedDB -> Using IndexedDB,因为水平有限,翻译不当的地方请大家批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值