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

雷蒙.卡登 (Raymond Camden)
Raymond Camden

引言

IndexedDB 是將資料儲存在用戶端的強大方式。如果您還沒看過,我很推薦您閱讀這個主題的 MDN 教學課程。本文假設您對 API 和功能有一定程度的瞭解。即使您未曾看過 IndexedDB,希望本文中的示範能帶給您具體的使用方法。

我們的示範是對公司內部網路應用程式概念驗證的簡單證明。這個應用程式可讓員工搜尋其他員工。為了提供更快速流暢的使用體驗,系統會將員工資料庫複製到用戶端的電腦,並使用 IndexedDB 儲存。示範只是提供單一員工記錄的自動完成式搜尋和顯示功能,但更棒的是,一旦資料提供給用戶端,我們也能透過其他方式使用這些資料。以下概述應用程式所需進行的操作。

  1. 我們必須設定並初始化索引資料庫的執行個體。大多數情況下,在 Chrome 和 Firefox 中都能順利執行,這有點困難。
  2. 我們需要查看是否有任何資料,如果沒有資料,也請下載資料。我們通常可以透過 AJAX 呼叫執行此操作。在我們的示範中,我們建立了簡單的公用程式類別,可以快速產生假資料。應用程式必須識別建立此資料的時間,並防止使用者在那之前使用該資料。這項操作只需執行一次。使用者下次執行應用程式時,就不需要執行這項程序。更進階的示範將處理用戶端與伺服器之間的同步處理作業,但這個示範較著重於 UI 層面。
  3. 應用程式準備就緒後,我們就可以使用 jQuery UI 的「Autocomplete」控制項,與 IndexedDB 保持同步。雖然「自動完成」控制項允許基本的資料清單和陣列,但利用 API 允許任何資料來源。我們將說明如何使用這個資料庫來連結 Google 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 啟用。旁邊的 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 顯示員工詳細資料。這種做法稍後才會使用,但我們可以現在繼續編譯範本,讓做法一個很順利。我們已將指令碼區塊設定為可識別的處理列類型。這個做法沒有那麼精確,但可讓您更輕鬆地顯示動態 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 都含有相關物件的集合。我們的示範簡單,只需要一個 ObjectStore,就稱為「employee」。當我們首次開啟已建立索引的資料庫,或是變更程式碼中的版本時,系統會執行需要升級的事件。我們可用它來設定 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 (物件儲存空間陣列),確認其中是否包含員工。如果沒有,就直接執行。 createIndex 呼叫很重要。我們必須告知 IndexedDB 要用哪些方法 (鍵除外) 擷取資料。我們將使用一個稱為「搜尋值」的值稍後會進一步說明。

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

進入正題前,讓我們先快速複習一下以下情況開啟資料庫我們會檢查物件存放區是否存在。如果沒有,我們就會加以建立。最後,我們呼叫了 handleSeed 函數。現在,我們將注意示範資料出現部分。

給我一些資料!

如本文開頭所述,這個示範會重新建立一個內部網路樣式應用程式,需要儲存所有已知員工的副本。這通常涉及建立以伺服器為基礎的 API,可傳回員工人數,並提供擷取一批記錄的方式。您可以想像一個簡單的服務支援起始計數,並且一次傳回 100 人。這可能會在使用者關閉其他操作時,於背景以非同步方式執行。

在示範中,我們做了一些簡單的事。請查看索引資料庫中的物件數量 (如果有的話)。假如使用者數量低於特定,我們只會建立假使用者。否則,我們將視為種子部分完成,並能啟用示範的自動完成部分。我們來看看把手種子。

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()

執行計數 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 不支援以不區分大小寫的方式查詢項目。因此,我們會在新的屬性「搜尋值」中,複製姓氏欄位。如果你記得,我們說的就是資料索引。

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

由於這是用戶端專屬的修改內容,因此是在本程序完成,而非在後端伺服器 (在本範例中為虛擬後端伺服器) 進行。

如要以高效的方式執行資料庫新增作業,您應針對所有批次寫入重複使用交易。假如您每次的寫入作業都會建立一個新交易,瀏覽器可能會針對每筆交易建立新的磁碟資料,導致新增大量項目時可能會導致效能不穩定 (例如「寫 1000 個物件:1 分鐘,寫入 1000 個物件」)。

種子完成後,就會啟動應用程式的下一部分 - setupAutoComplete。

建立 Autocomplete

現在開始有趣的部分,也就是使用 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」控制項來定義來源屬性,以便對任何可能的需求進行自訂,甚至是索引資料庫資料。API 會提供要求 (基本上是在表單欄位中輸入的內容) 以及回應回呼。您必須負責將結果陣列傳回該回呼。

第一個動作是隱藏 displayEmployee div。這可用來顯示個人員工,如果員工先前已載入過,即可清除它。現在可以開始搜尋了。

我們會先建立唯讀交易、稱為 result 的陣列,以及一個會直接將結果傳遞至自動完成控制項的 oncomplete 處理常式。

為了找出符合輸入內容的項目,我們來使用 StackOverflow 使用者 Fong-Wan Chau 的小費:我們會使用以輸入內容為基礎的索引範圍做為下邊邊界,並加上字母 z 做為範圍邊界。請注意,我們使用小寫字詞,才會符合我們輸入的小寫資料。

完成之後,我們就能開啟遊標 (如同執行資料庫查詢) 並反覆查看結果。jQuery 使用者介面的自動完成控制項可讓您傳回任何類型的資料,但至少需要值鍵。我們將值設為格式妥善的名稱版本。我們也會退還所有人。您稍後將瞭解原因。首先,以下是自動完成功能實際運作的螢幕截圖。我們現在為 jQuery UI 使用 Vader 主題。

本身就是將 Google 索引資料庫比對的結果傳回自動完成功能。但我們也希望支援在選取相符項目時,顯示相符項目的詳細資料檢視畫面。我們在建立自動完成功能時,指定了選取處理常式,該處理常式會利用先前使用的 Handlebars 範本。