使用 IndexedDB 将数据绑定界面元素

Raymond Camden
Raymond Camden

简介

IndexedDB 是一种在客户端存储数据的强大方式。如果您尚未阅读过相关内容,建议您阅读有关该主题的实用 MDN 教程。本文假定您对这些 API 和功能有一些基本了解。即使您之前从未接触过 IndexedDB,希望本文中的演示也能让您了解它的用途。

我们的演示是一个简单的公司内网应用概念验证。该应用将允许员工搜索其他员工。为了提供更快捷、更流畅的体验,员工数据库会复制到客户端的机器上,并使用 IndexedDB 进行存储。该演示只是提供了一个自动补全样式的搜索功能,并显示了一条员工记录,但好处是,一旦这些数据在客户端上可用,我们还可以通过多种其他方式使用这些数据。以下是我们的应用需要执行的基本操作。

  1. 我们必须设置并初始化 IndexedDB 的实例。在大多数情况下,这很简单,但要让它在 Chrome 和 Firefox 中都能正常运行,却有点棘手。
  2. 我们需要查看是否有任何数据,如果没有,则下载数据。通常,这会通过 AJAX 调用来完成。在演示中,我们创建了一个简单的实用程序类,用于快速生成虚假数据。应用需要识别何时创建这些数据,并阻止用户在此之前使用这些数据。这是一项一次性操作。下次用户运行该应用时,便无需再经历此过程。更高级的演示会处理客户端和服务器之间的同步操作,但此演示更侧重于界面方面。
  3. 应用准备就绪后,我们就可以使用 jQuery UI 的自动补全控件与 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 代码块开始:

$(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。objectStore 类似于数据库表。一个 IndexedDB 可能有多个 objectStore,每个 objectStore 都包含一组相关对象。我们的演示很简单,只需要一个名为“employee”的 objectStore。首次打开 indexedDB 或更改代码中的版本时,系统会运行 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 的函数。现在,我们来关注演示的数据播种部分。

给我一些数据!

如本文简介中所述,此演示将重新创建一个内网风格的应用,该应用需要存储所有已知员工的副本。通常,这需要创建一个基于服务器的 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 的异步特性,我们设置了一个简单的变量 done,用于跟踪添加操作。我们循环遍历并插入虚构人员。该函数的源代码可在下载内容中找到,但它会返回一个如下所示的对象:

{
  firstname: "Random Name",
  lastname: "Some Random Last Name",
  department: "One of 8 random departments",
  email: "first letter of firstname+lastname@fakecorp.com"
}

仅凭这一点,就足以定义一个人。但为了能够搜索数据,我们有一个特殊要求。IndexedDB 不提供以不区分大小写的方式查找项的方法。因此,我们将 lastname 字段复制到一个新属性 searchkey 中。如果您还记得,这就是我们之前提到的应该作为数据索引创建的键。

// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();

由于这是针对特定客户端的修改,因此是在此处完成的,而不是在后端服务器(在我们的示例中,是虚构的后端服务器)上完成的。

为了以高效的方式执行数据库添加操作,您应该对所有批量写入操作重复使用该事务。如果您为每次写入都创建一个新事务,浏览器可能会为每个事务执行一次磁盘写入,这会导致在添加大量内容时性能极差(想想看,“写入 1,000 个对象需要 1 分钟”有多糟糕)。

种子数据填充完成后,应用的下一部分 setupAutoComplete 就会启动。

创建自动补全

现在进入最有趣的环节 - 与 jQuery UI 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 UI 的自动补全控件允许您定义一个源属性,该属性可以自定义以满足任何可能的需求,甚至包括我们的 IndexedDB 数据。该 API 会为您提供请求(基本上就是输入到表单字段中的内容)和响应回调。您负责将结果数组发送回该回调。

我们首先隐藏 displayEmployee div。这用于显示单个员工,如果之前已加载一个员工,则将其清除。现在,我们可以开始搜索了。

我们首先创建一个只读事务、一个名为 result 的数组,以及一个 oncomplete 处理程序,该处理程序只是将结果传递给自动补全控件。

为了找到与输入内容匹配的项,我们来利用 StackOverflow 用户 Fong-Wan Chau 的一个技巧:我们使用基于输入内容的索引范围作为下限边界,并使用输入内容加上字母 z 作为上限边界。另请注意,我们将该字词转换为小写,以便与我们输入的小写数据匹配。

完成后,我们可以打开一个游标(可以将其视为运行数据库查询),并遍历结果。jQuery UI 的自动补全控件允许您返回所需的任何类型的数据,但至少需要一个值键。我们将该值设置为格式规范的名称。我们还会返回整个 Person 对象。稍后您就会知道原因。首先,下面是自动补全功能的屏幕截图。我们使用的是 jQuery UI 的 Vader 主题。

仅凭这一点,就足以将 IndexedDB 匹配结果返回给自动补全功能。但我们还希望支持在选择某场比赛时显示该比赛的详情视图。我们在创建自动补全功能时指定了一个选择处理程序,该处理程序会使用前面提到的 Handlebars 模板。