以下是我在學(xué)習(xí)IndexedDB時(shí)做的總結(jié),為了方便以后使用時(shí)速查,特意記錄如下:
一. IndexedDB介紹
IndexedDB是一個(gè)基于JavaScript的面向?qū)ο蟮氖聞?wù)型數(shù)據(jù)庫系統(tǒng)。
IndexedDB 分別為同步和異步訪問提供了單獨(dú)的 API 。同步 API 本來是要用于僅供 Web Workers 內(nèi)部使用,但是還沒有被任何瀏覽器所實(shí)現(xiàn)。異步 API 在 Web Workers 內(nèi)部和外部都可以使用。
二. 異步API
異步 API 方法調(diào)用完后會(huì)立即返回,而不會(huì)阻塞調(diào)用線程。要異步訪問數(shù)據(jù)庫,要調(diào)用 window 對象 indexedDB 屬性的 open() 方法。該方法返回一個(gè) IDBRequest 對象 (IDBOpenDBRequest);異步操作通過在 IDBRequest 對象上觸發(fā)事件來和調(diào)用程序進(jìn)行通信。
以下是異步使用IndexedDB時(shí)的相關(guān)類
- IDBFactory 提供了對數(shù)據(jù)庫的訪問。這是由全局對象 indexedDB 實(shí)現(xiàn)的接口,因而也是該 API 的入口。
- IDBCursor 遍歷對象存儲(chǔ)空間和索引。
- IDBCursorWithValue 遍歷對象存儲(chǔ)空間和索引并返回游標(biāo)的當(dāng)前值。
- IDBDatabase 表示到數(shù)據(jù)庫的連接。只能通過這個(gè)連接來拿到一個(gè)數(shù)據(jù)庫事務(wù)。
- IDBEnvironment 提供了到客戶端數(shù)據(jù)庫的訪問。它由 window 對象實(shí)現(xiàn)。
- IDBIndex 提供了到索引元數(shù)據(jù)的訪問。
- IDBKeyRange 定義鍵的范圍。
- IDBObjectStore 表示一個(gè)對象存儲(chǔ)空間。
- IDBOpenDBRequest 表示一個(gè)打開數(shù)據(jù)庫的請求。
- IDBRequest 提供了到數(shù)據(jù)庫異步請求結(jié)果和數(shù)據(jù)庫的訪問。這也是在你調(diào)用一個(gè)異步方法時(shí)所得到的。
- IDBTransaction 表示一個(gè)事務(wù)。你在數(shù)據(jù)庫上創(chuàng)建一個(gè)事務(wù),指定它的范圍(例如你希望訪問哪一個(gè)對象存儲(chǔ)空間),并確定你希望的訪問類型(只讀或?qū)懭耄?/li>
- IDBVersionChangeEvent 表明數(shù)據(jù)庫的版本號(hào)已經(jīng)改變。
早期版本中的下面這些現(xiàn)接口已經(jīng)被刪除:
- IDBVersionChangeRequest 表示更改數(shù)據(jù)庫版本號(hào)的請求。更改數(shù)據(jù)庫版本的方式已經(jīng)自此改變了(調(diào)用 IDBFactory.open() 而不需要再調(diào)用 IDBDatabase.setVersion()),并且 IDBOpenDBRequest 接口現(xiàn)在具有已經(jīng)移除的 IDBVersionChangeRequest 的功能。
- IDBDatabaseException 表示在執(zhí)行數(shù)據(jù)庫操作時(shí)可能碰到的異常情況。
規(guī)范里面還定義了 API 的同步版本。同步 API 還沒有在任何瀏覽器中得以實(shí)現(xiàn)。它原本是要和 WebWorkers 一起使用的。
三. 操作數(shù)據(jù)庫
1.打開數(shù)據(jù)庫
示例代碼:
// 打開我們的數(shù)據(jù)庫
var openDBRequest = window.indexedDB.open("MyTestDatabase", 3);
indexedDB是IDBFactory類型,它有一個(gè)open方法,用于打開或者創(chuàng)建并打開指定的數(shù)據(jù)庫(如果該數(shù)據(jù)庫不存在,則會(huì)被創(chuàng)建;如果已經(jīng)存在,則被打開)。
open 請求不會(huì)立即打開數(shù)據(jù)庫或者開始一個(gè)事務(wù)。 對 open() 函數(shù)的調(diào)用會(huì)返回一個(gè)我們可以作為事件來處理的包含 result(成功的話)或者錯(cuò)誤值的 IDBOpenDBRequest 對象。在 IndexedDB 中的大部分異步方法做的都是同樣的事情 - 返回一個(gè)包含 result 或錯(cuò)誤的 IDBRequest 對象。open 函數(shù)的結(jié)果是一個(gè) IDBDatabase對象的實(shí)例。
該 open 方法接受第二個(gè)參數(shù),就是數(shù)據(jù)庫的版本號(hào)。這樣我們就可以更新數(shù)據(jù)庫的 schema ,也就是說如果我們打開的數(shù)據(jù)庫不是我們期望的最新版本的話,我們可以對 object store 進(jìn)行創(chuàng)建或是刪除。在這種情況下,我們實(shí)現(xiàn)一個(gè) onupgradeneeded 處理函數(shù),在一個(gè)允許操作 object stores 的 versionchange 事務(wù)中 - 我們在后面的 更新數(shù)據(jù)庫的版本號(hào)中會(huì)提到更多有關(guān)這方面的內(nèi)容。
2. 添加處理函數(shù)
為了得打開數(shù)據(jù)庫的結(jié)果,我們需要給openDBRequest添加相關(guān)的事件處理程序:
openDBRequest.onerror = function(event) {
// Do something with request.errorCode!
};
openDBRequest.onsuccess = function(event) {
// Do something with request.result!
};
這兩個(gè)函數(shù)的哪一個(gè),onsuccess() 還是 onerror(),會(huì)被調(diào)用呢?如果一切順利的話,一個(gè) success 事件(即一個(gè) type 屬性被設(shè)置成 "success" 的 DOM 事件)會(huì)被觸發(fā),使用 request 作為它的 target。 一旦它被觸發(fā)的話,相關(guān) request 的 onsuccess() 函數(shù)就會(huì)被觸發(fā),使用 success 事件作為它的參數(shù)。 否則,如果不是所有事情都成功的話,一個(gè) error 事件(即 type 屬性被設(shè)置成 "error" 的 DOM 事件) 會(huì)在 request 上被觸發(fā)。這將會(huì)觸發(fā)使用 error 事件作為參數(shù)的 onerror() 方法。
3. 創(chuàng)建和更新數(shù)據(jù)庫版本號(hào)
要更新數(shù)據(jù)庫的 schema,也就是創(chuàng)建或者刪除對象存儲(chǔ)空間,需要實(shí)現(xiàn) onupgradeneeded 處理程序,這個(gè)處理程序?qū)?huì)作為一個(gè)允許你處理對象存儲(chǔ)空間的 versionchange 事務(wù)的一部分被調(diào)用。
openDBRequest.onupgradeneeded = function(event) {
// 更新對象存儲(chǔ)空間和索引 ....
};
在onupgradeneeded處理程序中,openDBRequest的result屬性是IDBDatabase類型;
在數(shù)據(jù)庫第一次被打開時(shí)或者當(dāng)指定的版本號(hào)高于當(dāng)前被持久化的數(shù)據(jù)庫的版本號(hào)時(shí),這個(gè) versionchange 事務(wù)將被創(chuàng)建。
版本號(hào)是一個(gè) unsigned long long 數(shù)字,這意味著它可以是一個(gè)非常大的整數(shù)。
4. 構(gòu)建數(shù)據(jù)庫
現(xiàn)在來構(gòu)建數(shù)據(jù)庫。IndexedDB 使用對象存儲(chǔ)空間而不是表,并且一個(gè)單獨(dú)的數(shù)據(jù)庫可以包含任意數(shù)量的對象存儲(chǔ)空間。每當(dāng)一個(gè)值被存儲(chǔ)進(jìn)一個(gè)對象存儲(chǔ)空間時(shí),它會(huì)被和一個(gè)鍵相關(guān)聯(lián)。鍵的提供可以有幾種不同的方法,這取決于對象存儲(chǔ)空間是使用 key path 還是 key generator。
你也可以使用對象存儲(chǔ)空間持有的對象,不是基本數(shù)據(jù)類型,在任何對象存儲(chǔ)空間上創(chuàng)建索引。索引可以讓你使用被存儲(chǔ)的對象的屬性的值來查找存儲(chǔ)在對象存儲(chǔ)空間的值,而不是用對象的鍵來查找。
此外,索引具有對存儲(chǔ)的數(shù)據(jù)執(zhí)行簡單限制的能力。通過在創(chuàng)建索引時(shí)設(shè)置 unique 標(biāo)記,索引可以確保不會(huì)有兩個(gè)具有同樣索引 key path 值的對象被儲(chǔ)存。因此,舉例來說,如果你有一個(gè)用于持有一組 people 的對象存儲(chǔ)空間,并且你想要確保不會(huì)有兩個(gè)擁有同樣 email 地址的 people,你可以使用一個(gè)帶有 unique 標(biāo)識(shí)的索引來確保這些。
這聽起來可能有點(diǎn)混亂,但下面這個(gè)簡單的例子應(yīng)該可以演示這些個(gè)概念:
// 我們的客戶數(shù)據(jù)看起來像這樣。
const customerData = [
{ ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
{ ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" }
];
const dbName = "the_name";
var request = indexedDB.open(dbName, 2);
request.onerror = function(event) {
// 錯(cuò)誤處理程序在這里。
};
request.onupgradeneeded = function(event) {
//request(即event.target)的result屬性是IDBDatabase類型;
var db = event.target.result;
// 創(chuàng)建一個(gè)對象存儲(chǔ)空間來持有有關(guān)我們客戶的信息。
// 我們將使用 "ssn" 作為我們的 key path 因?yàn)樗WC是唯一的。
// createObjectStore()方法返回的是IDBObjectStore類型的對象;
var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
// 創(chuàng)建一個(gè)索引來通過 name 搜索客戶。
// 可能會(huì)有重復(fù)的,因此我們不能使用 unique 索引。
objectStore.createIndex("name", "name", { unique: false });
// 創(chuàng)建一個(gè)索引來通過 email 搜索客戶。
// 我們希望確保不會(huì)有兩個(gè)客戶使用相同的 email 地址,因此我們使用一個(gè) unique 索引。
objectStore.createIndex("email", "email", { unique: true });
// 在新創(chuàng)建的對象存儲(chǔ)空間中保存值
for (var i in customerData) {
objectStore.add(customerData[i]);
}
};
正如前面提到的,onupgradeneeded 是我們唯一可以修改數(shù)據(jù)庫結(jié)構(gòu)的地方。在這里面,我們可以創(chuàng)建和刪除對象存儲(chǔ)空間以及構(gòu)建和刪除索引。
對象存儲(chǔ)空間僅調(diào)用 createObjectStore() 就可以創(chuàng)建。這個(gè)方法使用存儲(chǔ)空間的名稱,和一個(gè)對象參數(shù)。即便這個(gè)參數(shù)對象是可選的,它還是非常重要的,因?yàn)樗梢宰屇愣x重要的可選屬性和完善你希望創(chuàng)建的對象存儲(chǔ)空間的類型。在我們的示例中,我們請求了一個(gè)名為“customers” 的對象存儲(chǔ)空間并且定義了一個(gè) 使得存儲(chǔ)空間中每個(gè)單獨(dú)的對象都是唯一的屬性作為 key path。在這個(gè)示例中的屬性是 “ssn”,因?yàn)樯鐣?huì)安全號(hào)碼被確保是唯一的。被存儲(chǔ)在對象存儲(chǔ)空間中的所有對象都必須存在“ssn”。
我們也請求了一個(gè)名為 “name” 的著眼于存儲(chǔ)的對象的 name 屬性的索引。如同 createObjectStore(),createIndex() 使用了一個(gè)完善了我們希望創(chuàng)建的索引類型的可選的 options 對象。添加一個(gè)不帶 name 屬性的對象也會(huì)成功,但是這個(gè)對象不會(huì)出現(xiàn)在 "name" 索引中。
我們現(xiàn)在可以使用存儲(chǔ)的用戶對象的 ssn 直接從對象存儲(chǔ)空間中把它們提取出來,或者通過使用索引來使用他們的 name 進(jìn)行提取。
5. 增刪改查
在你可以對新數(shù)據(jù)庫做任何事情之前,你需要開始一個(gè)事務(wù)。事務(wù)來自于數(shù)據(jù)庫對象,而且你必須指定你想讓這個(gè)事務(wù)跨越哪些對象存儲(chǔ)空間。另外,你需要決定你是否將要對數(shù)據(jù)庫進(jìn)行更改或者你只是需要從它里面進(jìn)行讀取。雖然事務(wù)具有三種模式(只讀,讀寫,和版本變更),在可以的情況下你最好還是使用只讀事務(wù),因?yàn)樗鼈兛梢圆l(fā)運(yùn)行。
5.1 向數(shù)據(jù)庫中增加數(shù)據(jù)
如果你剛剛創(chuàng)建了一個(gè)數(shù)據(jù)庫,你可能想往里面寫點(diǎn)東西??雌饋頃?huì)像下面這樣:
//transaction()方法返回的是IDBTransaction類型的對象
var transaction = db.transaction(["customers"], "readwrite");
// 注意: 舊版實(shí)驗(yàn)性的實(shí)現(xiàn)使用不建議使用的常量 IDBTransaction.READ_WRITE 而不是 "readwrite"。
// 如果你想支持這樣的實(shí)現(xiàn),你只要這樣寫就可以了:
// var transaction = db.transaction(["customers"], IDBTransaction.READ_WRITE);
transaction() 方法接受三個(gè)參數(shù)(雖然兩個(gè)是可選的)并返回一個(gè)IDBTransaction類型的事務(wù)對象。第一個(gè)參數(shù)是事務(wù)希望跨越的對象存儲(chǔ)空間的列表。如果你希望事務(wù)能夠跨越所有的對象存儲(chǔ)空間你可以傳入一個(gè)空數(shù)組。如果你沒有為第二個(gè)參數(shù)指定任何內(nèi)容,你得到的是只讀事務(wù)。因?yàn)檫@里我們是想要寫入所以我們需要傳入 "readwrite" 標(biāo)識(shí)。
現(xiàn)在我們已經(jīng)有了一個(gè)事務(wù),我們需要理解它的生命周期。事務(wù)和事件循環(huán)的聯(lián)系非常密切。如果你創(chuàng)建了一個(gè)事務(wù)但是并沒有使用它就返回給事件循環(huán),那么事務(wù)將變得無效。保持事務(wù)活躍的唯一方法就是在其上構(gòu)建一個(gè)請求。當(dāng)請求完成時(shí)你將會(huì)得到一個(gè) DOM 事件,并且,假設(shè)請求成功了,你將會(huì)有另外一個(gè)機(jī)會(huì)在回調(diào)中來延長這個(gè)事務(wù)。如果你沒有延長事務(wù)就返回到了事件循環(huán),那么事務(wù)將會(huì)變得不活躍,依此類推。只要還有待處理的請求事務(wù)就會(huì)保持活躍。事務(wù)生命周期真的很簡單但是可能需要一點(diǎn)時(shí)間你才能對它變得習(xí)慣。還有就是來幾個(gè)例子也會(huì)有所幫助。如果你開始看到 TRANSACTION_INACTIVE_ERR 錯(cuò)誤代碼,那么你已經(jīng)把某些事情搞亂了。
事務(wù)可以接收三種不同類型的 DOM 事件: error,abort,以及 complete。我們已經(jīng)討論過 error事件冒泡,所以一個(gè)事務(wù)要接收所有可能產(chǎn)生錯(cuò)誤事件的請求所產(chǎn)生的錯(cuò)誤事件。更微妙的一點(diǎn)是一個(gè) error 的默認(rèn)行為是終止發(fā)生錯(cuò)誤的事務(wù)。除非你在 error 事件上通過調(diào)用 preventDefault() 處理了這個(gè)錯(cuò)誤,整個(gè)事務(wù)被回滾了。這樣的設(shè)計(jì)迫使你去思考和處理錯(cuò)誤,但是如果細(xì)粒度的錯(cuò)誤處理太過繁瑣的話,你也可以總是對數(shù)據(jù)庫添加一個(gè)總的錯(cuò)誤處理程序。如果你不處理一個(gè)錯(cuò)誤事件或者你在事務(wù)中調(diào)用 abort(),那么事務(wù)被回滾并且有關(guān)事物的一個(gè) abort 事件被觸發(fā)。否則,在所有的未處理請求都完成后,你將得到一個(gè) complete 事件。如果你正在做大量的數(shù)據(jù)庫操作,那么追蹤事務(wù)而不是單個(gè)的請求當(dāng)然可以幫助你進(jìn)行決斷。
現(xiàn)在你有了一個(gè)事務(wù)了,你將需要從它拿到一個(gè)對象存儲(chǔ)空間。事務(wù)只能讓你拿到一個(gè)你在創(chuàng)建事務(wù)時(shí)已經(jīng)指定過的對象存儲(chǔ)空間。然后你可以增加所有你需要的數(shù)據(jù)。
// 當(dāng)所有的數(shù)據(jù)都被增加到數(shù)據(jù)庫時(shí)執(zhí)行一些操作
transaction.oncomplete = function(event) {
alert("All done!");
};
transaction.onerror = function(event) {
// 不要忘記進(jìn)行錯(cuò)誤處理!
};
//因?yàn)閠ransaction可以跨越多個(gè)對象存儲(chǔ)空間(IDBObjectStore類型的對象),所以,當(dāng)需要獲取特定的對象存儲(chǔ)空間時(shí)還需要再通過事務(wù)對象transaction的objectStore()方法來獲取;
var objectStore = transaction.objectStore("customers");
for (var i in customerData) {
var request = objectStore.add(customerData[i]);
request.onsuccess = function(event) {
// event.target.result == customerData[i].ssn
};
}
產(chǎn)生自 add() 調(diào)用的請求的 result 是被添加的值的鍵。因此在這種情況下,它應(yīng)該等于被添加的對象的 ssn 屬性, 因?yàn)閷ο蟠鎯?chǔ)空間使用 ssn 屬性作為 key path。 注意 add() 函數(shù)要求數(shù)據(jù)庫中不能已經(jīng)有相同鍵的對象存在。如果你正在試圖修改一個(gè)現(xiàn)有條目,或者你并不關(guān)心是否有一個(gè)同樣的條目已經(jīng)存在,使用 put()函數(shù)。
5.2從數(shù)據(jù)庫中刪除數(shù)據(jù)
刪除數(shù)據(jù)是非常類似的:
var request = objectStore.delete("444-44-4444");
request.onsuccess = function(event) {
// 刪除數(shù)據(jù)成功!
};
5.3 從數(shù)據(jù)庫中獲取數(shù)據(jù)
現(xiàn)在數(shù)據(jù)庫里已經(jīng)有了一些信息,你可以通過幾種方法對它進(jìn)行提取。首先是簡單的 get()。你需要提供鍵來提取值,像這樣:
var request = objectStore.get("444-44-4444");
request.onerror = function(event) {
// 錯(cuò)誤處理!
};
request.onsuccess = function(event) {
// 對 request.result 做些操作!
alert("Name for SSN 444-44-4444 is " + request.result.name);
};
5.4 使用游標(biāo)
使用 get() 要求你知道你想要檢索哪一個(gè)鍵。如果你想要遍歷對象存儲(chǔ)空間中的所有值,那么你可以使用游標(biāo)??雌饋頃?huì)像下面這樣:
//openCursor()方法返回一個(gè)IDBRequest類型的請求對象cursorRequest,如果該請求對象cursorRequest成功,則cursorRequest的result屬性是IDBCursorWithValue類型的對象;
var cursorRequest = objectStore.openCursor();
cursorRequest.onsuccess = function(event) {
//cursorRequest的result屬性是IDBCursorWithValue類型的對象
var cursor = event.target.result;
if (cursor) {
alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor.continue();
}
else {
alert("No more entries!");
}
};
openCursor() 函數(shù)需要幾個(gè)參數(shù)。首先,你可以使用一個(gè) key range 對象來限制被檢索的項(xiàng)目的范圍。第二,你可以指定你希望進(jìn)行迭代的方向。在上面的示例中,我們在以升序迭代所有的對象。游標(biāo)成功的回調(diào)有點(diǎn)特別。游標(biāo)對象本身是請求的 result (上面我們使用的是簡寫形式,所以是 event.target.result)。然后實(shí)際的 key 和 value 可以根據(jù)游標(biāo)對象的 key 和 value 屬性被找到。如果你想要保持繼續(xù)前行,那么你必須調(diào)用游標(biāo)上的 continue() 。當(dāng)你已經(jīng)到達(dá)數(shù)據(jù)的末尾時(shí)(或者沒有匹配 openCursor() 請求的條目)你仍然會(huì)得到一個(gè)成功回調(diào),但是 result 屬性是 undefined。
默認(rèn)情況下,每個(gè)游標(biāo)只發(fā)起一次請求。要想發(fā)起另一次請求,必須調(diào)用游標(biāo)的下面2個(gè)方法之一:
- continue(key):移動(dòng)到結(jié)果集中的下一項(xiàng)。參數(shù) key 是可選的,不指定這個(gè)參數(shù),游標(biāo)移動(dòng) 到下一項(xiàng);指定這個(gè)參數(shù),游標(biāo)會(huì)移動(dòng)到指定鍵的位置。
-
advance(count):向前移動(dòng) count 指定的項(xiàng)數(shù)。
這兩個(gè)方法都會(huì)導(dǎo)致游標(biāo)使用相同的請求,因此相同的 onsuccess 和 onerror 事件處理程序也會(huì)
得到重用。