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

Raymond Camden
Raymond Camden

簡介

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

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

  1. 我們必須設定及初始化 IndexedDB 的例項。這項操作大多很簡單,但要讓這項操作在 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 可能會有許多物件集合,每個物件集合都會保留相關物件的集合。我們的示範很簡單,只需要一個稱為「employee」的物件儲存庫。當 indexedDB 首次開啟,或是您在程式碼中變更版本時,系統會執行 onupgradeneeded 事件。我們可以使用這個值來設定物件儲存庫。

// 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,以便傳回員工人數,並提供擷取大量記錄的方法。您可以想像,有個簡單的服務支援起始計數,每次傳回 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 並未提供區分大小寫的查詢項目方式。因此,我們會將姓氏欄位複製到新的屬性 searchkey 中。如您還記得,這是我們說應該建立為資料索引的鍵。

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

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

如要以高效的方式執行資料庫新增作業,請針對所有批次寫入作業重複使用交易。如果您為每次寫入作業建立新的交易,瀏覽器可能會為每個交易執行磁碟寫入作業,這會導致在新增大量項目時,效能會變得非常糟糕 (想想「寫入 1,000 個物件的時間為 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 範本的自動完成功能時,指定了選取處理常式。