深入探讨 HTML5 IndexedDB





5.00/5 (5投票s)
在本文中,我将回顾一项技术,该技术解决了应用程序拼图中一个重要部分的问题——在客户端管理用户特定数据的存储和检索——这项技术叫做“IndexedDB”。
多年来,Web 已经越来越多地从内容存储库转变为功能齐全的应用程序市场。属于“HTML5”旗帜的技术套件,其基本目标是具备构建这种新型软件的能力。在本文中,我将回顾一项技术,该技术解决了应用程序拼图中一个重要部分的问题——在客户端管理用户特定数据的存储和检索——这项技术叫做“IndexedDB”。
什么是 IndexedDB?
IndexedDB 本质上是浏览器中的一个持久化数据存储——一个客户端数据库。与常规的关系型数据库一样,它维护着存储记录的索引,开发人员可以使用 IndexedDB JavaScript API 通过键或通过查找索引来定位记录。每个数据库都按“源”(origin)进行范围划分,即创建数据库的站点的域。
IndexedDB 也是 Web 标准如何演进的一个绝佳示例。通过标准工作组和 HTML5 Labs(一个发布各种 HTML5 规范原型实现并允许您试用和提供反馈的网站),IndexedDB 即将准备好投入实际的网站使用。
如果您是 IndexedDB 的新手,请从这里开始
- 在 IETestDrive 上的 Cookbook 演示
- 在 MSDN 上的开发者指南
- 在 W3C 上的规范
现在,让我们通过构建自己的应用程序来深入了解。
设置您的开发环境
从安装开始
- 点击 此处 的“立即下载原型!”链接以下载原型。
- 解压下载的文件。
- 如果您运行的是 32 位 Windows 版本,请运行 vcredist_x86.exe。
- 通过从提升的命令提示符运行以下命令来注册“sqlcejse40.dll”
regsvr32 sqlcejse40.dll
如果一切顺利,您应该会看到此屏幕
Internet Explorer 10 Platform Preview 附带对 IndexedDB 的支持。或者,您可以获取最新版本的 Google Chrome 或 Firefox,这样就万事俱备了。
构建一个离线笔记应用程序
我们将为笔记 Web 应用程序构建一个客户端数据层:
从数据模型的角度来看,它尽可能地简单。该应用程序允许用户编写文本笔记并使用特定的关键字对其进行标记。每条笔记将有一个唯一的标识符作为其键,除了笔记文本之外,它还将与一组标签字符串相关联。
下面是用 JavaScript 对象字面量表示的示例笔记对象
var note = {
id: 1,
text: "Note text.",
tags: ["sample", "test"]
};
我们将构建一个具有以下接口的 NotesStore 对象
var NotesStore = {
init: function(callback) {
},
addNote: function(text, tags, callback) {
},
listNotes: function(callback) {
}
};
每个方法的作用应该很明显。所有方法调用都会异步执行(也就是说,当通过回调报告结果时),并且在有结果要返回给调用者时,接口会接受一个回调的引用,该回调将在有结果时被调用。让我们看看使用索引数据库高效实现此对象需要什么。
测试 IndexedDB
与 IndexedDB API 交互时的根对象称为 indexedDB。您可以检查此对象是否存在,以查看当前浏览器是否支持 IndexedDB。像这样:
if(window[“indexedDB”] === undefined) {
// nope, no IndexedDB!
} else {
// yep, we’re good to go!
}
或者,您可以使用 Modernizr JavaScript 库来测试对 IndexedDB 的支持,如下所示:
if(Modernizr.indexeddb) {
// yep, go indexeddb!
} else {
// bleh! No joy!
}
异步请求
异步 API 调用通过所谓的“请求”对象工作。当进行异步 API 调用时,它会返回一个“请求”对象的引用,该对象公开两个事件——onsuccess 和 onerror。
典型的调用看起来像这样:
var req = someAsyncCall();
req.onsuccess = function() {
// handle success case
};
req.onerror = function() {
// handle error
};
当您使用 indexedDB API 时,最终会难以跟踪所有回调。为了稍微简化一下,我将定义并使用一个小的实用例程来抽象“请求”模式:
var Utils = {
errorHandler: function(cb) {
return function(e) {
if(cb) {
cb(e);
} else {
throw e;
}
};
},
request: function (req, callback, err_callback) {
if (callback) {
req.onsuccess = function (e) {
callback(e);
};
}
req.onerror = errorHandler(err_callback);
}
};
现在,我可以这样编写我的异步调用:
Utils.request(someAsyncCall(), function(e) {
// handle completion of call
});
创建和打开数据库
通过调用 indexedDB 对象的 open 方法来创建/打开数据库。
这是 NotesStore 对象 init 方法的实现:
var NotesStore = {
name: “notes-db”,
db: null,
ver: “1.0”,
init: function(callback) {
var self = this;
callback = callback || function () { };
Utils.request(window.indexedDB.open(“open”, this.name), function(e) {
self.db = e.result;
callback();
});
},
...
open 方法在数据库已存在时打开数据库。如果不存在,它将创建一个新的数据库。您可以将其视为代表数据库连接的对象。当此对象被销毁时,与数据库的连接将终止。
现在数据库已经存在,让我们创建其余的数据库对象。但首先,您需要熟悉一些重要的 IndexedDB 概念。
对象存储
对象存储是 IndexedDB 中“表”的对应物,来自关系型数据库世界。所有数据都存储在对象存储中,是存储的主要单位。
一个数据库可以包含多个对象存储,每个存储都是一组记录。每条记录都是一个简单的键/值对。键必须唯一标识一条记录,并且可以自动生成。对象存储中的记录按键升序自动排序。最后,对象存储只能在“版本更改”事务的上下文中创建和删除。(稍后将详细介绍。)
键和值
对象存储中的每条记录都由一个“键”唯一标识。键可以是数组、字符串、日期或数字。为了比较,数组大于字符串,字符串大于日期,日期大于数字。
键可以是“内联”键,也可以不是。通过“内联”,我们指示 IndexedDB 特定记录的键实际上是值对象本身的一部分。例如,在我们的笔记存储示例中,每条笔记对象都有一个 id 属性,其中包含该笔记的唯一标识符。这是“内联”键的一个例子——键是值对象的一部分。
无论何时键是“内联”的,我们还必须指定一个“键路径”(key path)——一个字符串,表示如何从值对象中提取键值。
例如,“notes”对象的键路径是字符串“id”,因为可以通过访问“id”属性从笔记实例中提取键。但这种方案允许键值存储在值对象的成员层次结构的任意深度。考虑以下示例值对象:
var product = {
info: {
name: “Towel”,
type: “Indispensable hitchhiker item”,
},
identity: {
server: {
value: “T01”
},
client: {
value: “TC01”
},
},
price: “Priceless”
};
在这里,可以使用以下键路径:
identity.client.value
数据库版本控制
IndexedDB 数据库与一个版本字符串相关联。Web 应用程序可以利用它来确定特定客户端上的数据库是否具有最新的结构。
当您更改数据库的数据模型并希望将这些更改传播到拥有旧版本数据模型的现有客户端时,这非常有用。您只需为新结构更改版本号,并在用户下次运行您的应用程序时进行检查。如果需要,则升级结构,迁移数据,并更改版本号。
版本号更改必须在“版本更改”事务的上下文中执行。在此之前,让我们快速回顾一下“事务”是什么。
事务
与关系型数据库一样,IndexedDB 也在事务的上下文中执行所有 I/O 操作。事务是通过连接对象创建的,并实现原子、持久的数据访问和修改。事务对象有两个关键属性:
1. 范围
范围决定了事务可以影响数据库的哪些部分。这基本上有助于 IndexedDB 实现确定在事务期间应用的隔离级别。您可以将范围简单地视为将构成事务一部分的表(称为“对象存储”)的列表。
2. 模式
事务模式决定了允许在该事务中执行何种 I/O 操作。模式可以是:
- a. 只读
允许对属于事务范围的对象执行“读取”操作。
- 读/写
允许对属于事务范围的对象执行“读取”和“写入”操作。
- 版本更改
“版本更改”模式允许“读取”和“写入”操作,还允许创建和删除对象存储和索引。
事务对象会自动提交,除非它们已被明确中止。事务对象会公开事件以通知客户端:
- 何时完成
- 何时中止以及
- 何时超时
创建对象存储
我们的笔记存储数据库将只包含一个对象存储来记录笔记列表。如前所述,对象存储必须在“版本更改”事务的上下文中创建。
让我们继续扩展 NotesStore 对象的 init 方法以包含对象存储的创建。我已将更改的部分以粗体突出显示。
var NotesStore = {
name: “notes-db”,
store_name: “notes-store”,
store_key_path: “id”,
db: null,
ver: “1.0”,
init: function (callback) {
var self = this;
callback = callback || function () { };
Utils.request(window.indexedDB.open(“open”, this.name), function (e) {
self.db = e.result;
// if the version of this db is not equal to
// self.version then change the version
if (self.db.version !== self.version) {
Utils.request(self.db.setVersion(self.ver), function (e2) {
var txn = e2.result;
// create object store
self.db.createObjectStore(self.store_name,
self.store_key_path,
true);
txn.commit();
callback();
});
} else {
callback();
}
});
},
...
对象存储是通过在数据库对象上调用 createObjectStore 方法创建的。第一个参数是对象存储的名称。紧随其后的是标识键路径的字符串,最后是一个布尔标志,指示在添加新记录时是否应由数据库自动生成键值。
向对象存储添加数据
可以通过在对象存储上调用 put 方法向对象存储添加新记录。对象存储实例的引用可以通过事务对象检索。让我们实现 NotesStore 对象的 addNote 方法,看看如何添加新记录:
...
addNote: function (text, tags, callback) {
var self = this;
callback = callback || function () { };
var txn = self.db.transaction(null, TransactionMode.ReadWrite);
var store = txn.objectStore(self.store_name);
Utils.request(store.put({
text: text,
tags: tags
}), function (e) {
txn.commit();
callback();
});
},
...
此方法可分解为以下步骤:
- 通过调用数据库对象的 transaction 方法来启动新事务。第一个参数是要构成事务一部分的对象存储的名称。传递 null 会导致数据库中的所有对象存储都成为范围的一部分。第二个参数指示事务模式。这基本上是一个我们已声明的数字常量,如下所示:
// IndexedDB transaction mode constants var TransactionMode = { ReadWrite: 0, ReadOnly: 1, VersionChange: 2 };
- 创建事务后,我们通过事务对象的 objectStore 方法获取相关对象存储的引用。
- 一旦我们有了对象存储,添加新记录只需发出对对象存储的 put 方法的异步 API 调用,传入要添加到存储的新对象。请注意,我们不为新笔记对象的 id 字段传递值。由于我们在创建对象存储时为 auto-generate 参数传递了 true,IndexedDB 实现应该会负责为新记录自动分配唯一标识符。
- 一旦异步 put 调用成功完成,我们便提交事务。
使用游标运行查询
IndexedDB 枚举对象存储记录的方式是使用“游标”对象。游标可以迭代底层对象存储或索引的记录。游标具有以下关键属性:
- 位于索引或对象存储中的记录的范围。
- 一个源,引用游标正在迭代的索引或对象存储。
- 一个位置,指示游标在给定记录范围内的当前位置。
虽然游标的概念相当直接,但鉴于所有 API 调用都是异步的,编写实际迭代对象存储的代码有些棘手。让我们实现 NotesStore 对象的 listNotes 方法,看看代码的样子。
listNotes: function (callback) {
var self = this,
txn = self.db.transaction(null, TransactionMode.ReadOnly),
notes = [],
store = txn.objectStore(self.store_name);
Utils.request(store.openCursor(), function (e) {
var cursor = e.result,
iterate = function () {
Utils.request(cursor.move(), function (e2) {
// if "result" is true then we have data else
// we have reached end of line
if (e2.result) {
notes.push(cursor.value);
// recursively get next record
iterate();
}
else {
// we are done retrieving rows; invoke callback
txn.commit();
callback(notes);
}
});
};
// set the ball rolling by calling iterate for the first row
iterate();
});
},
让我们分解一下这个实现:
- 首先,我们通过调用数据库对象的 transaction 方法来获取事务对象。请注意,这次我们指示需要“只读”事务。
- 接下来,我们通过事务对象的 objectStore 方法检索对象存储的引用。
- 然后,我们对对象存储调用 openCursor API 发出异步调用。这里的棘手之处在于,游标中的每次记录迭代本身就是一个异步操作!为了防止代码因回调过多而变得混乱,我们定义了一个名为 iterate 的本地函数来封装遍历游标中每条记录的逻辑。
- 此 iterate 函数对游标对象的 move 方法进行异步调用,并在回调中递归地再次调用自身,如果检测到还有更多行需要检索。一旦检索完游标中的所有行,我们最终会调用调用者传入的回调方法,并将检索到的数据作为参数传入。
深入了解!
尽管您可能认为如此,但这绝不是对 API 的全面介绍!我只涵盖了:
- 今天可用的客户端存储实现选项
- IndexedDB API 的各种关键方面,包括:
- 测试浏览器是否支持它
- 管理异步 API 调用
- 创建/打开数据库
- API 的关键部分,包括对象存储、键/值、版本控制和事务
- 创建对象存储
- 向对象存储添加记录
- 使用游标枚举对象存储
希望它足够深入!
现在,如果您已准备好了解更多内容,那么 W3C 规范文档 是一个很好的参考,而且足够简洁易读!我鼓励您进行实验——拥有对客户端的可用数据库打开了 Web 应用程序一系列新的可能性。
另一个很好的资源是 IE Test Drive 网站上的 IndexedDB/AppCache 示例。此示例涵盖了一种场景,其中两个规范相互补充,为用户提供丰富的体验……即使她未连接到互联网。该示例还演示了 IE10 中的新功能,如 CSS3 3D 转换和 CSS3 过渡。
玩得开心!