HTML5 的一个重要特性是本地数据持久性,它使用户可以在线和离线访问 Web 应用程序。此外,本地数据持久性使移动应用程序更灵敏,使用的带宽更少,并且可以在低带宽场景中更高效地工做。HTML5 提供了一些本地数据持久性选项。第一个选项是 localstorage,它支持您使用一个简单的键值对来存储数据。IndexedDB(一个更增强大的选项)支持您本地存储大量对象,并使用健壮的数据访问机制检索数据。数据库
IndexedDB API 取代了 Web Storage API,后者在 HTML5 规范中已不推荐使用。(但一些领先的浏览器仍然支持 Web Storage,其中包括苹果公司的 Safari 和 Opera Web 浏览器)与 Web Storage 相比,IndexedDB 具备多个优点,其中包括索引、事务处理和健壮的查询功能。本文将经过一系列的示例来展现如何管理 IndexedDB 数据库。(参见 下载 一节,获取示例的完整源代码。)json
重要概念浏览器
一个网站可能有一个或多个 IndexedDB 数据库,每一个数据库必须具备唯一的名称。缓存
一个数据库可包含一个或多个对象存储。一个对象存储(由一个名称唯一标识)是一个记录集合。每一个记录有一个键 和一个值。该值是一个对象,可拥有一个或多个属性。键可能基于某个键生成器,从一个键路径衍生出来,或者是显式设置。一个键生成器自动生成唯一的连续正整数。键路径定义了键值的路径。它能够是单个 JavaScript 标识符或多个由句点分隔的标识符。服务器
规范中包含一个异步 API 和一个同步 API。同步 API 用于 Web 浏览器中。异步 API 使用请求和回调。并发
在如下示例中,输出附加到一个具备 ID result 的 div 标记上。要更新 result 元素,可在每一个数据操做期间清除并设置 innerHTML 属性。每一个示例 JavaScript 函数由 HTML 按钮的一个 onclick 事件调用。dom
处理错误或异常和调试异步
全部异步请求都有一个 onsuccess 回调和一个 onerror 回调,前者在数据库操做成功时调用,后者在一个操做未成功时调用。清单 1 是一个 onerror 回调的示例。函数
清单 1. 异步错误处理函数工具
request.onerror = function(e) {
// handle error
...
console.log("Database error: " + e.target.errorCode);
};
在使用 IndexedDB API 时,使用 JavaScript try/catch 块是一个不错的想法。此功能对处理可能在数据库操做以前发生的错误和异常颇有用,好比在数据库未打开时尝试读取或操做数据,或者在另外一个读/写事务已打开时尝试写入数据。
IndexedDB 很难调试和排除故障,由于在许多状况下,错误消息是泛泛的,缺少信息价值。在开发应用程序时,可使用 console.log 和 JavaScript 调试工具,好比用于 Mozilla Firefox 的 Firebug,或者 Chrome 内置的 Developer Tools。对于任何 JavaScript 密集型应用程序,这些工具的价值是无可估量的,它们尤为适用于使用 IndexedDB API 的 HTML5 应用程序。
回页首
使用数据库
一个数据库一次只能有一个版本。在首次建立数据库时,它的初始版本编号为 0。建立数据库以后,数据库(和它的对象存储)只能经过一种称为 versionchange 的特殊类型的事务来更改。要在建立数据库后更改它,必须打开具备更高版本的数据库。此操做会触发 upgradeneeded 事件。修改数据库或对象存储的代码必须位于 upgradeneeded 事件处理函数中。
清单 2 中的代码段展现了如何建立数据库:调用 open 方法并传递数据库名称。若是不存在具备指定名称的数据库,则会建立该数据库。
清单 2. 建立一个新的数据库
function createDatabase() {
var openRequest = localDatabase.indexedDB.open(dbName);
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
console.log("Database created");
localDatabase.db = openRequest.result;
};
openRequest.onupgradeneeded = function (evt) {
...
};
}
要删除现有数据库,能够调用 deleteDatabase 方法,并传递要删除的数据库名称,如 清单 3 中所示。
清单 3. 删除现有数据库
function deleteDatabase() {
var deleteDbRequest = localDatabase.indexedDB.deleteDatabase(dbName);
deleteDbRequest.onsuccess = function (event) {
// database deleted successfully
};
deleteDbRequest.onerror = function (e) {
console.log("Database error: " + e.target.errorCode);
};
}
清单 4 中的代码段展现了如何打开与现有数据库的链接。
清单 4. 打开数据库的最新版本
function openDatabase() {
var openRequest = localDatabase.indexedDB.open("dbName");
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
localDatabase.db = openRequest.result;
};
}
建立、删除和打开数据库就是这么简单。如今是时候使用对象存储了。
回页首
使用对象存储
对象存储是一个数据记录集合。要在现有数据库中建立一个新对象存储,则须要对现有数据库进行版本控制。为此,请打开要进行版本控制的数据库。除了数据库名称以外,open 方法还接受版本号做为第二个参数。若是但愿建立数据库的一个新版本(也就是说,要建立或修改一个对象存储),只需打开具备现有数据库版本更高的数据库。这会调用 onupgradeneeded 事件处理函数。
要建立一个对象存储,能够在数据库对象上调用 createObjectStore 方法,如 清单 5 中所示。
清单 5. 建立对象存储
function createObjectStore() {
var openRequest = localDatabase.indexedDB.open(dbName, 2);
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
localDatabase.db = openRequest.result;
};
openRequest.onupgradeneeded = function (evt) {
var employeeStore = evt.currentTarget.result.createObjectStore
("employees", {keyPath: "id"});
};
}
咱们已经了解了对象存储的工做原理。接下来,让咱们看看索引 如何引用包含数据的对象存储。
回页首
使用索引
除了使用键来检索对象存储中的记录,还可以使用代索引的字段来检索记录。对象存储可具备一个或多个索引。索引是一种特殊的对象存储,它引用包含数据的对象存储,在更改所引用的对象存储时(也就是添加、修改或删除记录时)自动更新。
要建立一个索引,必须使用 清单 5 中所示的方法对数据库进行版本控制。索引能够是唯一的,也能够是不唯一的。唯一索引要求索引中的全部值都是唯一的,好比使用一个电子邮件地址字段。当某个值能够重复出现时,须要使用非唯一索引,好比城市、州或国家。清单 6 中的代码段展现了如何在 employee 对象的 state 字段上建立一个非唯一索引,在 ZIP code 字段上建立一个非唯一索引,并在 email address 字段上建立一个唯一索引:
清单 6. 建立索引
function createIndex() {
var openRequest = localDatabase.indexedDB.open(dbName, 2);
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
db = openRequest.result;
};
openRequest.onupgradeneeded = function (evt) {
var employeeStore = evt.currentTarget.result.objectStore("employees");
employeeStore.createIndex("stateIndex", "state", { unique: false });
employeeStore.createIndex("emailIndex", "email", { unique: true });
employeeStore.createIndex("zipCodeIndex", "zip_code", { unique: false })
};
}
接下来,您将使用事务对对象存储执行一些操做。
回页首
使用事务
您须要使用事务在对象存储上执行全部读取和写入操做。相似于关系数据库中的事务的工做原理,IndexedDB 事务提供了数据库写入操做的一个原子集合,这个集合要么彻底提交,要么彻底不提交。IndexedDB 事务还拥有数据库操做的一个停止和提交工具。
表 1 列出并描述了 IndexedDB 提供的事务模式。
表 1. IndexedDB 事务模式
模式 描述
readonly 提供对某个对象存储的只读访问,在查询对象存储时使用。
readwrite 提供对某个对象存储的读取和写入访问权。
versionchange 提供读取和写入访问权来修改对象存储定义,或者建立一个新的对象存储。
默认的事务模式为 readonly。您可在任何给定时刻打开多个并发的 readonly 事务,但只能打开一个 readwrite 事务。出于此缘由,只有在数据更新时才考虑使用 readwrite 事务。单独的(表示不能打开任何其余并发事务)versionchange 事务操做一个数据库或对象存储。能够在 onupgradeneeded 事件处理函数中使用 versionchange 事务建立、修改或删除一个对象存储,或者将一个索引添加到对象存储。
要在 readwrite 模式下为 employees 对象存储建立一个事务,可使用语句:var transaction = db.transaction("employees", "readwrite");。
清单 7 中的 JavaScript 函数展现了如何使用一个事务,使用键来检索 employees 对象存储中的一条特定的员工记录。
清单 7. 使用键获取一个特定的记录
function fetchEmployee() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees").objectStore("employees");
store.get("E3").onsuccess = function(event) {
var employee = event.target.result;
if (employee == null) {
result.value = "employee not found";
}
else {
var jsonStr = JSON.stringify(employee);
result.innerHTML = jsonStr;
}
};
}
}
catch(e){
console.log(e);
}
}
清单 8 中的 JavaScript 函数展现了如何使用一个事务,以使用 emailIndex 索引而不是对象存储键来检索 employees 对象存储中的特定员工记录。
清单 8. 使用索引获取特定的记录
function fetchEmployeeByEmail() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var range = IDBKeyRange.only("john.adams@somedomain.com");
var store = localDatabase.db.transaction("employees")
.objectStore("employees");
var index = store.index("emailIndex");
index.get(range).onsuccess = function(evt) {
var employee = evt.target.result;
var jsonStr = JSON.stringify(employee);
result.innerHTML = jsonStr;
};
}
}
catch(e){
清单 9 是使用 readwrite 事务建立新员工记录的一个示例。
清单 9. 建立一条新员工记录
function addEmployee() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
var transaction = localDatabase.db.transaction("employees", "readwrite");
var store = transaction.objectStore("employees");
if (localDatabase != null && localDatabase.db != null) {
var request = store.add({
"id": "E5",
"first_name" : "Jane",
"last_name" : "Doh",
"email" : "jane.doh@somedomain.com",
"street" : "123 Pennsylvania Avenue",
"city" : "Washington D.C.",
"state" : "DC",
"zip_code" : "20500",
});
request.onsuccess = function(e) {
result.innerHTML = "Employee record was added successfully.";
};
request.onerror = function(e) {
console.log(e.value);
result.innerHTML = "Employee record was not added.";
};
}
}
catch(e){
console.log(e);
}
}
清单 10 是使用 readwrite 事务更新现有员工记录的一个示例。该示例更改了记录 ID 为 E3 的员工的电子邮件地址。
清单 10. 更新现有的员工记录
function updateEmployee() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
var transaction = localDatabase.db.transaction("employees", "readwrite");
var store = transaction.objectStore("employees");
var jsonStr;
var employee;
if (localDatabase != null && localDatabase.db != null) {
store.get("E3").onsuccess = function(event) {
employee = event.target.result;
// save old value
jsonStr = "OLD: " + JSON.stringify(employee);
result.innerHTML = jsonStr;
// update record
employee.email = "john.adams@anotherdomain.com";
var request = store.put(employee);
request.onsuccess = function(e) {
console.log("Added Employee");
};
request.onerror = function(e) {
console.log(e.value);
};
// fetch record again
store.get("E3").onsuccess = function(event) {
employee = event.target.result;
jsonStr = "
NEW: " + JSON.stringify(employee);
result.innerHTML = result.innerHTML + jsonStr;
}; // fetch employee again
}; // fetch employee first time
}
}
catch(e){
console.log(e);
}
}
清单 11 是一个建立或删除对象存储中的全部记录的 readwrite 事务的示例。像其余异步事务同样,clear 事务根据对象存储是否已清除来调用 onsuccess 或 onerror 回调。
清单 11. 清除对象存储事务
function clearAllEmployees() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees", "readwrite")
.objectStore("employees");
store.clear().onsuccess = function(event) {
result.innerHTML = "'Employees' object store cleared";
};
}
}
catch(e){
console.log(e);
}
}
这些示例演示了事务的一些常见用途。接下来您将看到 IndexedDB 中游标的工做原理。
回页首
使用游标
相似于关系数据库中游标的工做方式,IndexedDB 中的游标使您可以迭代一个对象存储中的记录。您还可使用对象存储的索引来迭代记录。IndexedDB 中的游标是双向的,因此您可向前和向后迭代记录,还能够跳过非唯一索引中的重复记录。openCursor 方法能够打开一个游标。它接受两个可选的参数,其中包括范围和方向。
清单 12 为 employees 对象存储打开一个游标,并迭代全部员工记录。
清单 12. 迭代全部员工记录
function fetchAllEmployees() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees")
.objectStore("employees");
var request = store.openCursor();
request.onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
接下来的这些示例将游标用于索引。表 2 列出并描述了 IndexedDB API 在为索引打开游标时提供的范围类型或过滤器。
表 2. IndexedDB API 在为索引打开游标时提供的范围类型或过滤器
范围类型或过滤器 描述
IDBKeyRange.bound 返回指定范围内的全部记录。这个范围有一个下边界和上边界。它还有两个可选的参数:lowerOpen 和 upperOpen,这两个参数代表下边界或上边界上的记录是否应包含在范围内。
IDBKeyRange.lowerBound 超过指定的边界值范围的全部记录。此范围有一个可选的参数 lowerOpen,代表下边界上的记录是否应包含在范围中。
IDBKeyRange.upperBound 返回指定的边界值以前的全部记录。它也有一个可选的 upperOpen 参数。
IDBKeyRange.only 仅返回与指定值匹配的记录。
清单 13 是迭代特定国家的全部员工记录的一个基本示例。这个查询最多见。它使您可以检索与特定条件匹配的全部记录。这个示例使用 stateIndex 和 IDBKeyRange.only 范围,返回与指定值(在本例中为 "New York")匹配的全部记录。
清单 13. 迭代纽约市内的全部员工记录。
function fetchNewYorkEmployees() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var range = IDBKeyRange.only("New York");
var store = localDatabase.db.transaction("employees")
.objectStore("employees");
var index = store.index("stateIndex");
index.openCursor(range).onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
清单 14 是一个使用 IDBKeyRange.lowerBound 范围的示例。它迭代邮政编码高于 92000 的全部员工。
清单 14. 使用 IDBKeyRange.lowerBound
function fetchEmployeeByZipCode1() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees").objectStore("employees");
var index = store.index("zipIndex");
var range = IDBKeyRange.lowerBound("92000");
index.openCursor(range).onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
清单 15 是一个使用 IDBKeyRange.upperBound 范围的示例。它迭代邮政编码基于 93000 的全部员工。
清单 15. 使用 IDBKeyRange.upperBound
function fetchEmployeeByZipCode2() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees").objectStore("employees");
var index = store.index("zipIndex");
var range = IDBKeyRange.upperBound("93000");
index.openCursor(range).onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
清单 16 是一个使用 IDBKeyRange.bound 范围的示例。它检索邮政编码值在 92000 与 92999(含)之间的全部员工。
清单 16. 使用 IDBKeyRange.bound
function fetchEmployeeByZipCode3() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees").objectStore("employees");
var index = store.index("zipIndex");
var range = IDBKeyRange.bound("92000", "92999", true, true);
index.openCursor(range).onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
这些示例代表,IndexedDB 中的游标功能与关系数据库中的游标功能相似。使用 IndexedDB 游标,可迭代一个对象存储中的记录和对象存储的某个索引的记录。IndexedDB 中的游标是双向的,这提供了额外的灵活性。
回页首
结束语
IndexedDB API 很是强大,您可使用它建立具备丰富本地存储数据的数据密集型应用程序(尤为是离线的 HTML5 Web 应用程序)。您还可使用 IndexedDB API 将数据缓存到本地,使传统的在线 Web 应用程序(尤为是移动 Web 应用程序)可以更快地运行和响应,从而消除每次从 Web 服务器检索数据的需求。例如,能够将选择列表的数据缓存在 IndexedDB 数据库中。
本文展现了如何管理 IndexedDB 数据库,包括建立数据库,删除数据库,以及创建与数据库的链接。本文还展现了 IndexedDB API 的许多更高级的功能,包括事务处理、索引和游标。您可使用这些展现的概念构建利用 IndexedDB API 的离线应用或移动 Web 应用程序。