简介
IndexedDB 是在客户端存储数据的有效方式。如果您还没有看过该主题,建议您阅读关于该主题的实用 MDN 教程。本文假定您对 API 和功能有基本的了解。即使您之前没有见过 IndexedDB,也希望本文中的演示可以帮助您了解使用 IndexedDB 的用法。
我们的演示是为公司设计的简单概念验证应用。通过该应用,员工可以搜索其他员工。为了提供更快速、更快速的体验,员工数据库被复制到客户端的机器,并使用 IndexedDB 进行存储。此演示仅以自动补全样式的方式搜索和显示单个员工记录,但更棒的是,当客户端上有这些数据后,我们也可以通过多种其他方式使用这些数据。下面简要说明了我们的应用需要执行的操作。
- 我们必须设置并初始化 IndexedDB 的实例。这在大多数情况下很简单,但事实证明,使其在 Chrome 和 Firefox 中都能运行并非易事。
- 我们需要查看是否有任何数据,如果没有,请下载数据。现在,这通常通过 AJAX 调用完成。在我们的演示中,我们创建了一个简单的实用程序类,用于快速生成虚假数据。应用需要识别何时创建这些数据,并在之前阻止用户使用数据。此操作只需执行一次。用户下次运行应用时,无需执行此流程。更高级的演示可以处理客户端和服务器之间的同步操作,但此演示更侧重于界面的方面。
- 当应用准备就绪后,我们可以使用 jQuery 界面的自动补全控件与 IndexedDB 同步。虽然自动补全控件支持使用基本的数据列表和数组,但它有一个 API 以支持任何数据源。我们将演示如何使用它来连接到 IndexedDB 数据。
入门指南
该演示分为多个部分,所以我们先简单了解一下 HTML 部分。
<form>
<p>
<label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
</p>
</form>
<div id="displayEmployee"></div>
不是很多,对吧?我们关注此界面的三个主要方面。第一个是自动补全功能字段“name”。加载时将停用,稍后将通过 JavaScript 启用。它旁边的 span 用于在初始种子期间向用户提供更新。最后,当您从自动建议中选择员工时,将使用 ID 为 displayEmployee 的 div。
现在,我们来看看 JavaScript。这里的内容有很多,我们将逐步讲解。完整的代码将在文末提供,以便您完整查看。
首先 - 对于支持 IndexedDB 的浏览器,我们需要考虑一些前缀问题。以下是 Mozilla 文档中的某些代码,经过修改,可为我们的应用程序所需的核心 IndexedDB 组件提供简单的别名。
window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
接下来,我们将在整个演示中使用几个全局变量:
var db;
var template;
现在,我们先从 jQuery 文档就绪代码块开始:
$(document).ready(function() {
console.log("Startup...");
...
});
我们的演示版使用 Handlebars.js 来显示员工详细信息。我们稍后不会用到它,但我们可以现在就编译模板,将其清除。我们已将脚本块设置为 Handlebars 可识别的类型。这并不是特别花哨,但 它可以让您更轻松地显示动态 HTML。
<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>
然后,它会在我们的 JavaScript 中重新编译,如下所示:
//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);
现在开始使用 IndexedDB。首先,我们打开它。
var openRequest = indexedDB.open("employees", 1);
打开与 IndexedDB 的连接使我们可以访问读取和写入数据,但在执行此操作之前,我们必须确保拥有 ObjectStore。对象存储就像数据库表。一个 IndexedDB 可能有多个对象存储,每个对象存储着一组相关对象。我们的演示很简单,只需要一个我们称之为“employee”的 ObjectStore。首次打开 indexDB 或更改代码中的版本时,系统会运行 onupgradeneeded 事件。我们可以用它来设置 objectStore。
// Handle setup.
openRequest.onupgradeneeded = function(e) {
console.log("running onupgradeneeded");
var thisDb = e.target.result;
// Create Employee
if(!thisDb.objectStoreNames.contains("employee")) {
console.log("I need to make the employee objectstore");
var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
objectStore.createIndex("searchkey", "searchkey", {unique: false});
}
};
openRequest.onsuccess = function(e) {
db = e.target.result;
db.onerror = function(e) {
alert("Sorry, an unforseen error was thrown.");
console.log("***ERROR***");
console.dir(e.target);
};
handleSeed();
};
在 onupgradeneeded
事件处理脚本块中,我们会检查对象存储数组“objectStoreNames”,以查看它是否包含 employee。如果没有,则直接执行此操作。createIndex 调用非常重要。我们必须告知 IndexedDB 使用键以外的其他哪些方法来检索数据。我们用一个称为 searchkey下文对此进行了详细介绍。
onungradeneeded
事件将在我们首次运行脚本时自动运行。执行后(或在将来的运行过程中跳过)后,系统会运行 onsuccess
处理程序。我们定义了一个简单(但又丑)的错误处理程序,然后调用 handleSeed
。
在继续之前,我们先快速回顾一下这里发生的情况。我们打开数据库检查对象存储是否存在。如果没有,我们会创建一个。最后,我们调用名为“handleSeed”的函数。现在,我们将注意力转向演示中的数据种子部分。
给我一些数据!
正如本文简介中所述,此演示将重新创建 Intranet 样式的应用,该应用需要存储所有已知员工的副本。通常,这需要创建一个基于服务器的 API,该 API 可以返回员工数,并为我们提供检索批量记录的方法。您可以想象这样一个简单的服务,它支持开始计数,并且一次返回 100 人。此操作可以在用户执行其他任务时在后台异步运行。
在演示中,我们做了一些简单的改动。我们可以看到 IndexedDB 中有多少对象(如果有)。如果低于特定数值,我们将直接创建虚假用户。否则,我们会认为种子部分已完成,可以启用演示的自动补全部分。我们来看 handleSeed。
function handleSeed() {
// This is how we handle the initial data seed. Normally this would be via AJAX.
db.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
var count = e.target.result;
if (count == 0) {
console.log("Need to generate fake data - stand by please...");
$("#status").text("Please stand by, loading in our initial data.");
var done = 0;
var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
// Generate 1k people
for (var i = 0; i < 1000; i++) {
var person = generateFakePerson();
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
resp = employees.add(person);
resp.onsuccess = function(e) {
done++;
if (done == 1000) {
$("#name").removeAttr("disabled");
$("#status").text("");
setupAutoComplete();
} else if (done % 100 == 0) {
$("#status").text("Approximately "+Math.floor(done/10) +"% done.");
}
}
}
} else {
$("#name").removeAttr("disabled");
setupAutoComplete();
}
};
}
第一行有点复杂,因为我们把多个运算相互链接在一起,所以我们来分解一下:
db.transaction(["employee"], "readonly");
这将创建一个新的只读事务。使用 IndexedDB 的所有数据操作都需要某种类型的事务。
objectStore("employee");
获取员工对象存储区。
count()
运行 Count API,您可以猜到该 API 会执行计数。
onsuccess = function(e) {
完成后,执行此回调。在回调内,我们可以获取结果值,即对象数量。如果计数为零,我们开始种子过程。
我们使用前面提到的状态 div 向用户显示我们将开始获取数据的消息。由于 IndexedDB 的异步特性,我们设置了一个简单变量,用于跟踪添加操作。我们回过头来,插进来虚假的人。该函数的来源可在下载中找到,但它会返回如下所示的对象:
{
firstname: "Random Name",
lastname: "Some Random Last Name",
department: "One of 8 random departments",
email: "first letter of firstname+lastname@fakecorp.com"
}
这本身就足以定义一个人。但是,为了能够搜索到我们的数据,有一项特殊要求。IndexedDB 不支持以不区分大小写的方式查询项。因此,我们将姓氏字段复制到新属性 searchkey 中。如果您还记得的话,这就是我们说的应该创建为数据索引的键。
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
由于这是特定于客户端的修改,因此是在此处进行的,而不是在后端服务器(在本例中为虚构的后端服务器)上进行。
要高效地执行数据库添加操作,您应该对所有批量写入重复使用该事务。如果您每次写入都创建一个新事务,浏览器可能会为每个事务执行一次磁盘写入,这将导致添加大量项时的性能非常差(认为“1 分钟即可写入 1000 个对象”太糟糕了)。
种子完成后,将触发应用的下一部分 - setupAutoComplete。
创建自动补全
接下来到了有趣的环节 - 连接 jQuery 界面 Autocomplete 插件。与大多数 jQuery 界面一样,我们先从基本的 HTML 元素开始,然后通过对其调用构造函数方法来增强该元素。我们已将整个过程提取到一个名为 setupAutoComplete 的函数中。现在我们看一下该代码。
function setupAutoComplete() {
//Create the autocomplete
$("#name").autocomplete({
source: function(request, response) {
console.log("Going to look for "+request.term);
$("#displayEmployee").hide();
var transaction = db.transaction(["employee"], "readonly");
var result = [];
transaction.oncomplete = function(event) {
response(result);
};
// TODO: Handle the error and return to it jQuery UI
var objectStore = transaction.objectStore("employee");
// Credit: http://stackoverflow.com/a/8961462/52160
var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
var index = objectStore.index("searchkey");
index.openCursor(range).onsuccess = function(event) {
var cursor = event.target.result;
if(cursor) {
result.push({
value: cursor.value.lastname + ", " + cursor.value.firstname,
person: cursor.value
});
cursor.continue();
}
};
},
minLength: 2,
select: function(event, ui) {
$("#displayEmployee").show().html(template(ui.item.person));
}
});
}
此代码最复杂的部分是创建来源媒体资源。通过 jQuery 界面的自动补全控件,您可以定义来源属性,以便对来源属性进行自定义,以满足任何可能的需求,包括我们的 IndexedDB 数据。该 API 为您提供请求(基本上是输入到表单字段的内容)和响应回调函数。您负责将一组结果发送回该回调。
首先,我们隐藏 displayEmployee div。用于显示个别员工,如果之前已加载员工,则将其清除。现在,我们可以开始搜索了。
首先,我们创建一个只读事务、一个名为 result 的数组,以及一个仅将结果传递给自动补全控件的 oncomplete 处理程序。
为了查找与我们的输入匹配的项,我们可以使用 StackOverflow 用户 Fong-Wan Chau 的建议:我们使用基于输入的索引范围作为下限,使用输入加上字母 z 作为上限范围。另外请注意,我们将该字词小写以匹配我们输入的小写数据。
完成后,我们可以打开游标(可以将其视为运行数据库查询)并迭代结果。jQuery 界面的自动补全控件允许您返回所需的任何类型的数据,但至少需要一个值键。我们将该值设置为格式恰当的名称。我们还会返回整个人物。你马上就能明白为什么会这样。首先,这是一张使用自动补全功能的屏幕截图。我们为 jQuery 界面使用 Vader 主题。
这本身就足以将 IndexedDB 匹配的结果返回至自动补全。不过,我们还希望支持在选中匹配后显示匹配的详情视图。在创建使用之前 Handlebars 模板的自动补全时,我们指定了选择处理程序。