使用 IndexedDB 建立資料繫結 UI 元素

Raymond Camden
Raymond Camden

簡介

IndexedDB 是用於在用戶端儲存資料的強大工具。如果您還沒有查看,建議您閱讀相關的 MDN 教學課程。本文假設您對 API 和功能有一定程度的瞭解。即使您之前未曾接觸 IndexedDB,這篇文章的示範也能讓您瞭解如何使用這項技術。

我們的示範是針對公司內部網路應用程式進行的簡易概念驗證。員工可透過應用程式搜尋其他員工。為了提供更快速、更流暢的體驗,員工資料庫會複製到用戶端的電腦,並使用 IndexedDB 儲存。這個示範只提供自動完成式搜尋功能,並顯示單一員工記錄,但好處是,一旦客戶端有這項資料,我們就能以多種其他方式使用。以下是應用程式需要執行的作業基本大綱。

  1. 我們必須設定並初始化索引資料庫的執行個體。這項操作大多很簡單,但要讓這項操作在 Chrome 和 Firefox 中都有效,就必須使用一些技巧。
  2. 我們需要確認是否有任何資料,如果沒有,就下載資料。現在通常改由 AJAX 呼叫執行。在示範中,我們建立了簡單的公用程式類別,能夠快速產生假資料。應用程式需要在建立這項資料時加以辨識,並在那之前禁止使用者使用這項資料。這是一次性的操作,使用者下次執行應用程式時,就不需要完成這項程序。更進階的示範會處理用戶端與伺服器之間的同步處理作業,但本示範的重點更放在 UI 層面。
  3. 應用程式準備就緒後,我們就可以使用 jQuery UI 的 Autocomplete 控制項,與 IndexedDB 同步。雖然 Autocomplete 控制項可支援基本清單和資料陣列,但其 API 可支援任何資料來源。我們將示範如何使用這項功能連結至 IndexedDB 資料。

開始使用

這個示範包含多個部分,因此我們先從 HTML 部分開始。

<form>
  <p>
    <label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
    </p>
</form>

<div id="displayEmployee"></div>

不是很多,對吧?這個 UI 有三個主要面向值得我們關注。首先是用於自動完成功能的欄位「名稱」。這項元素會以停用狀態載入,並在稍後透過 JavaScript 啟用。而旁邊的跨度會在初始播放期間用於向使用者提供更新。最後,當您從自動建議中選取員工時,系統會使用 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。objectStore 就像資料庫資料表。一個 IndexedDB 可能會有許多物件集合,每個物件集合都會保留相關物件的集合。我們的示範操作簡單,只需要一個稱為「員工」的 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 說明,除了鍵之外,我們還會使用哪些方法擷取資料。我們會使用一個名為「搜尋鍵」的指令稍後會進一步說明這項功能。

系統會在第一次執行指令碼時自動執行 onungradeneeded 事件。執行後,或在日後執行時略過。onsuccess 處理常式便會執行。我們已定義簡單 (且難看) 的錯誤處理常式,然後呼叫 handleSeed

在繼續之前,讓我們快速複習一下目前的情況。我們會開啟資料庫。我們會檢查物件儲存空間是否存在。如果沒有,我們會自行建立。最後,我們會呼叫名為 handleSeed 的函式。接下來,我們將重點放在示範資料播種的部分。

請提供資料!

如本文前言所述,這個示範會重新建立內部網路風格的應用程式,該應用程式需要儲存所有已知員工的副本。通常這會涉及建立伺服器型 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();

由於這是用於特定用戶端的修改,因此是在這個位置進行,而非在後端伺服器 (或在本例中為虛構的後端伺服器) 上執行。

如要以高效能的方式執行資料庫新增作業,請針對所有批次寫入作業重複使用交易。如果您為每次寫入作業建立新的交易,瀏覽器可能會為每個交易執行磁碟寫入作業,這會導致在新增大量項目時,效能會變得非常糟糕 (想想「寫入 1000 個物件的時間為 1 分鐘」- 糟糕)。

種子完成後,應用程式會觸發下一個部分 - setupAutoComplete。

建立自動完成功能

現在來談談有趣的部分,那就是使用 jQuery UI Autocomplete 外掛程式建構內容。與大部分的 jQuery UI 一樣,我們會先從基本 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 的 Autocomplete 控制項定義來源屬性,並視需求自訂,甚至可用於 IndexedDB 資料。API 會提供要求 (基本上是在表單欄位中輸入的內容) 和回應回呼。您必須負責將結果陣列傳回至該回呼。

首先,我們會隱藏 displayEmployee div,用於顯示個別員工,並在先前載入的員工清除時,清除該員工。就能開始搜尋了

我們首先建立一個只讀交易,也就是一個名為 result 的陣列,以及一個 oncomplete 處理常式,這個處理常式只會將結果傳遞至自動完成控制項。

為了找出符合輸入內容的項目,我們採用 StackOverflow 使用者 Fong-Wan Chau 提供的提示:我們使用以輸入內容為依據的索引範圍做為下限邊界,並將輸入內容加上字母 z 做為上限邊界。請注意,我們會將該字詞改為小寫,藉此比對我們輸入的小寫資料。

完成後,我們可以開啟游標 (就像執行資料庫查詢),並重複執行結果。jQuery UI 的自動完成控制項可讓您傳回任何類型的資料,但至少需要一個值鍵。我們將值設為格式正確的名稱。我們也會傳回整個人。您稍後就會瞭解原因。首先,請看這張自動完成功能的螢幕截圖。我們使用 jQuery UI 的 Vader 主題。

這項功能本身就足以將 IndexedDB 比對結果傳回至 autocomplete。但我們也想支援在選取比對項目時,顯示相符項目的詳細資料檢視畫面。我們在建立使用 Handlebars 範本的自動完成功能時,指定了選取處理常式。