簡介
IndexedDB 是用於在用戶端儲存資料的強大工具。如果您尚未查看,建議您閱讀相關的 MDN 教學課程。本文假設您具備 API 和功能的基本知識。即使您之前未曾接觸 IndexedDB,這篇文章的示範也能讓您瞭解如何使用這項技術。
我們的示範是針對公司內部網應用程式進行的簡易概念驗證。員工可透過應用程式搜尋其他員工。為了提供更快速、更流暢的體驗,員工資料庫會複製到用戶端的電腦,並使用 IndexedDB 儲存。這個示範只提供自動完成式搜尋功能,並顯示單一員工記錄,但好處是,一旦客戶端有這項資料,我們就能以多種其他方式使用。以下是應用程式需要執行的作業基本大綱。
- 我們必須設定及初始化 IndexedDB 的例項。這項操作大多很簡單,但要讓這項操作在 Chrome 和 Firefox 中都有效,就必須使用一些技巧。
- 我們需要確認是否有任何資料,如果沒有,就下載資料。這項作業通常會透過 AJAX 呼叫完成。在本示範中,我們建立了簡單的工具類別,用於快速產生假資料。應用程式需要在建立這項資料時加以辨識,並在那之前禁止使用者使用這項資料。這項操作只需執行一次。下次使用者執行應用程式時,就不需要再執行這項程序。較進階的示範會處理用戶端和伺服器之間的同步作業,但這個示範較著重於 UI 方面。
- 應用程式準備就緒後,我們就可以使用 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 範本的自動完成功能時,指定了選取處理常式。