IndexedDB 是一個(gè)瀏覽器內(nèi)建的數(shù)據(jù)庫(kù),它比 ?localStorage
? 強(qiáng)大得多。
localStorage
? 相比,它可以存儲(chǔ)更大的數(shù)據(jù)量。對(duì)于傳統(tǒng)的 客戶端-服務(wù)器 應(yīng)用,這些功能通常是沒(méi)有必要的。IndexedDB 適用于離線應(yīng)用,可與 ServiceWorkers 和其他技術(shù)相結(jié)合使用。
根據(jù)規(guī)范 https://www.w3.org/TR/IndexedDB 中的描述,IndexedDB 的本機(jī)接口是基于事件的。
我們還可以在基于 promise 的包裝器(wrapper),如 https://github.com/jakearchibald/idb 的幫助下使用 async/await
。這要方便的多,但是包裝器并不完美,它并不能替代所有情況下的事件。因此,我們先練習(xí)事件(events),在理解了 IndexedDB
之后,我們將使用包裝器。
數(shù)據(jù)在哪兒?
從技術(shù)上講,數(shù)據(jù)通常與瀏覽器設(shè)置、擴(kuò)展程序等一起存儲(chǔ)在訪問(wèn)者的主目錄中。
不同的瀏覽器和操作系統(tǒng)級(jí)別的用戶都有各自獨(dú)立的存儲(chǔ)。
要想使用 IndexedDB,首先需要 open
(連接)一個(gè)數(shù)據(jù)庫(kù)。
語(yǔ)法:
let openRequest = indexedDB.open(name, version);
name
? —— 字符串,即數(shù)據(jù)庫(kù)名稱。version
? —— 一個(gè)正整數(shù)版本,默認(rèn)為 ?1
?(下面解釋)。數(shù)據(jù)庫(kù)可以有許多不同的名稱,但是必須存在于當(dāng)前的源(域/協(xié)議/端口)中。不同的網(wǎng)站不能相互訪問(wèn)對(duì)方的數(shù)據(jù)庫(kù)。
調(diào)用之后會(huì)返回 openRequest
對(duì)象,我們需要監(jiān)聽(tīng)該對(duì)象上的事件:
success
?:數(shù)據(jù)庫(kù)準(zhǔn)備就緒,?openRequest.result
? 中有了一個(gè)數(shù)據(jù)庫(kù)對(duì)象“Database Object”,我們應(yīng)該將其用于進(jìn)一步的調(diào)用。error
?:打開(kāi)失敗。upgradeneeded
?:數(shù)據(jù)庫(kù)已準(zhǔn)備就緒,但其版本已過(guò)時(shí)(見(jiàn)下文)。IndexedDB 具有內(nèi)建的“模式(scheme)版本控制”機(jī)制,這在服務(wù)器端數(shù)據(jù)庫(kù)中是不存在的。
與服務(wù)器端數(shù)據(jù)庫(kù)不同,IndexedDB 存在于客戶端,數(shù)據(jù)存儲(chǔ)在瀏覽器中。因此,開(kāi)發(fā)人員無(wú)法隨時(shí)都能訪問(wèn)它。因此,當(dāng)我們發(fā)布了新版本的應(yīng)用程序,用戶訪問(wèn)我們的網(wǎng)頁(yè),我們可能需要更新該數(shù)據(jù)庫(kù)。
如果本地?cái)?shù)據(jù)庫(kù)版本低于 open
中指定的版本,會(huì)觸發(fā)一個(gè)特殊事件 upgradeneeded
。我們可以根據(jù)需要比較版本并升級(jí)數(shù)據(jù)結(jié)構(gòu)。
當(dāng)數(shù)據(jù)庫(kù)還不存在時(shí)(從技術(shù)上講,其版本為 0
),也會(huì)觸發(fā) upgradeneeded
事件。因此,我們可以執(zhí)行初始化。
假設(shè)我們發(fā)布了應(yīng)用程序的第一個(gè)版本。
接下來(lái)我們就可以打開(kāi)版本 1
中的 IndexedDB 數(shù)據(jù)庫(kù),并在一個(gè) upgradeneeded
的處理程序中執(zhí)行初始化,如下所示:
let openRequest = indexedDB.open("store", 1);
openRequest.onupgradeneeded = function() {
// 如果客戶端沒(méi)有數(shù)據(jù)庫(kù)則觸發(fā)
// ...執(zhí)行初始化...
};
openRequest.onerror = function() {
console.error("Error", openRequest.error);
};
openRequest.onsuccess = function() {
let db = openRequest.result;
// 繼續(xù)使用 db 對(duì)象處理數(shù)據(jù)庫(kù)
};
之后不久,我們發(fā)布了第二個(gè)版本。
我們可以打開(kāi)版本 2
中的 IndexedDB 數(shù)據(jù)庫(kù),并像這樣進(jìn)行升級(jí):
let openRequest = indexedDB.open("store", 2);
openRequest.onupgradeneeded = function(event) {
// 現(xiàn)有的數(shù)據(jù)庫(kù)版本小于 2(或不存在)
let db = openRequest.result;
switch(event.oldVersion) { // 現(xiàn)有的 db 版本
case 0:
// 版本 0 表示客戶端沒(méi)有數(shù)據(jù)庫(kù)
// 執(zhí)行初始化
case 1:
// 客戶端版本為 1
// 更新
}
};
請(qǐng)注意:雖然我們目前的版本是 2
,onupgradeneeded
處理程序有針對(duì)版本 0
的代碼分支(適用于初次訪問(wèn),瀏覽器中沒(méi)有數(shù)據(jù)庫(kù)的用戶)和針對(duì)版本 1
的代碼分支(用于升級(jí))。
接下來(lái),當(dāng)且僅當(dāng) onupgradeneeded
處理程序沒(méi)有錯(cuò)誤地執(zhí)行完成,openRequest.onsuccess
被觸發(fā),數(shù)據(jù)庫(kù)才算是成功打開(kāi)了。
刪除數(shù)據(jù)庫(kù):
let deleteRequest = indexedDB.deleteDatabase(name)
// deleteRequest.onsuccess/onerror 追蹤(tracks)結(jié)果
我們無(wú)法使用較舊的 open 調(diào)用版本打開(kāi)數(shù)據(jù)庫(kù)
如果當(dāng)前用戶的數(shù)據(jù)庫(kù)版本比
open
調(diào)用的版本更高(比如當(dāng)前的數(shù)據(jù)庫(kù)版本為3
,我們卻嘗試運(yùn)行open(...2)
,就會(huì)產(chǎn)生錯(cuò)誤并觸發(fā)openRequest.onerror
)。
這很罕見(jiàn),但這樣的事情可能會(huì)在用戶加載了一個(gè)過(guò)時(shí)的 JavaScript 代碼時(shí)發(fā)生(例如用戶從一個(gè)代理緩存中加載 JS)。在這種情況下,代碼是過(guò)時(shí)的,但數(shù)據(jù)庫(kù)卻是最新的。
為了避免這樣的錯(cuò)誤產(chǎn)生,我們應(yīng)當(dāng)檢查
db.version
并建議用戶重新加載頁(yè)面。使用正確的 HTTP 緩存頭(header)來(lái)避免之前緩存的舊代碼被加載,這樣你就永遠(yuǎn)不會(huì)遇到此類問(wèn)題。
提到版本控制,有一個(gè)相關(guān)的小問(wèn)題。
舉個(gè)例子:
1
? 的我們的網(wǎng)站。這時(shí),有一個(gè)標(biāo)簽頁(yè)和版本為 1
的數(shù)據(jù)庫(kù)建立了一個(gè)連接,而另一個(gè)標(biāo)簽頁(yè)試圖在其 upgradeneeded
處理程序中將數(shù)據(jù)庫(kù)版本升級(jí)到 2
。
問(wèn)題是,這兩個(gè)網(wǎng)頁(yè)是同一個(gè)站點(diǎn),同一個(gè)源,共享同一個(gè)數(shù)據(jù)庫(kù)。而數(shù)據(jù)庫(kù)不能同時(shí)為版本 1
和版本 2
。要執(zhí)行版本 2
的更新,必須關(guān)閉對(duì)版本 1
的所有連接,包括第一個(gè)標(biāo)簽頁(yè)中的那個(gè)。
為了解決這一問(wèn)題,versionchange
事件會(huì)在“過(guò)時(shí)的”數(shù)據(jù)庫(kù)對(duì)象上觸發(fā)。我們需要監(jiān)聽(tīng)這個(gè)事件,關(guān)閉對(duì)舊版本數(shù)據(jù)庫(kù)的連接(還應(yīng)該建議訪問(wèn)者重新加載頁(yè)面,以加載最新的代碼)。
如果我們不監(jiān)聽(tīng) versionchange
事件,也不去關(guān)閉舊連接,那么新的連接就不會(huì)建立。openRequest
對(duì)象會(huì)產(chǎn)生 blocked
事件,而不是 success
事件。因此第二個(gè)標(biāo)簽頁(yè)無(wú)法正常工作。
下面是能夠正確處理并行升級(jí)情況的代碼。它安裝了 onversionchange
處理程序,如果當(dāng)前數(shù)據(jù)庫(kù)連接過(guò)時(shí)(數(shù)據(jù)庫(kù)版本在其他位置被更新)并關(guān)閉連接,則會(huì)觸發(fā)該處理程序。
let openRequest = indexedDB.open("store", 2);
openRequest.onupgradeneeded = ...;
openRequest.onerror = ...;
openRequest.onsuccess = function() {
let db = openRequest.result;
db.onversionchange = function() {
db.close();
alert("Database is outdated, please reload the page.")
};
// ……數(shù)據(jù)庫(kù)已經(jīng)準(zhǔn)備好,請(qǐng)使用它……
};
openRequest.onblocked = function() {
// 如果我們正確處理了 onversionchange 事件,這個(gè)事件就不應(yīng)該觸發(fā)
// 這意味著還有另一個(gè)指向同一數(shù)據(jù)庫(kù)的連接
// 并且在 db.onversionchange 被觸發(fā)后,該連接沒(méi)有被關(guān)閉
};
……換句話說(shuō),在這我們做兩件事:
db.onversionchange
? 監(jiān)聽(tīng)器會(huì)通知我們并行嘗試更新。openRequest.onblocked
? 監(jiān)聽(tīng)器通知我們相反的情況:在其他地方有一個(gè)與過(guò)時(shí)的版本的連接未關(guān)閉,因此無(wú)法建立新的連接。我們可以在 db.onversionchange
中更優(yōu)雅地進(jìn)行處理,提示訪問(wèn)者在連接關(guān)閉之前保存數(shù)據(jù)等。
或者,另一種方式是不在 db.onversionchange
中關(guān)閉數(shù)據(jù)庫(kù),而是使用 onblocked
處理程序(在瀏覽器新 tab 頁(yè)中)來(lái)提醒用戶,告訴他新版本無(wú)法加載,直到他們關(guān)閉瀏覽器其他 tab 頁(yè)。
這種更新沖突很少發(fā)生,但我們至少應(yīng)該有一些對(duì)其進(jìn)行處理的程序,至少在 onblocked
處理程序中進(jìn)行處理,以防程序默默卡死而影響用戶體驗(yàn)。
要在 IndexedDB
中存儲(chǔ)某些內(nèi)容,我們需要一個(gè) 對(duì)象庫(kù)。
對(duì)象庫(kù)是 IndexedDB 的核心概念,在其他數(shù)據(jù)庫(kù)中對(duì)應(yīng)的對(duì)象稱為“表”或“集合”。它是儲(chǔ)存數(shù)據(jù)的地方。一個(gè)數(shù)據(jù)庫(kù)可能有多個(gè)存儲(chǔ)區(qū):一個(gè)用于存儲(chǔ)用戶數(shù)據(jù),另一個(gè)用于商品,等等。
盡管被命名為“對(duì)象庫(kù)”,但也可以存儲(chǔ)原始類型。
幾乎可以存儲(chǔ)任何值,包括復(fù)雜的對(duì)象。
IndexedDB 使用 標(biāo)準(zhǔn)序列化算法 來(lái)克隆和存儲(chǔ)對(duì)象。類似于 JSON.stringify
,不過(guò)功能更加強(qiáng)大,能夠存儲(chǔ)更多的數(shù)據(jù)類型。
有一種對(duì)象不能被存儲(chǔ):循環(huán)引用的對(duì)象。此類對(duì)象不可序列化,也不能進(jìn)行 JSON.stringify
。
庫(kù)中的每個(gè)值都必須有唯一的鍵 key
。
鍵的類型必須為數(shù)字、日期、字符串、二進(jìn)制或數(shù)組。它是唯一的標(biāo)識(shí)符,所以我們可以通過(guò)鍵來(lái)搜索/刪除/更新值。
正如我們很快就會(huì)看到的,類似于 localStorage
,我們向存儲(chǔ)區(qū)添加值時(shí),可以提供一個(gè)鍵。但當(dāng)我們存儲(chǔ)對(duì)象時(shí),IndexedDB 允許將一個(gè)對(duì)象屬性設(shè)置為鍵,這就更加方便了?;蛘?,我們可以自動(dòng)生成鍵。
但我們需要先創(chuàng)建一個(gè)對(duì)象庫(kù)。
創(chuàng)建對(duì)象庫(kù)的語(yǔ)法:
db.createObjectStore(name[, keyOptions]);
請(qǐng)注意,操作是同步的,不需要 await
。
name
? 是存儲(chǔ)區(qū)名稱,例如 ?"books"
? 表示書(shū)。keyOptions
? 是具有以下兩個(gè)屬性之一的可選對(duì)象:keyPath
? —— 對(duì)象屬性的路徑,IndexedDB 將以此路徑作為鍵,例如 ?id
?。autoIncrement
? —— 如果為 ?true
?,則自動(dòng)生成新存儲(chǔ)的對(duì)象的鍵,鍵是一個(gè)不斷遞增的數(shù)字。如果我們不提供 keyOptions
,那么以后需要在存儲(chǔ)對(duì)象時(shí),顯式地提供一個(gè)鍵。
例如,此對(duì)象庫(kù)使用 id
屬性作為鍵:
db.createObjectStore('books', {keyPath: 'id'});
在 upgradeneeded
處理程序中,只有在創(chuàng)建數(shù)據(jù)庫(kù)版本時(shí),對(duì)象庫(kù)被才能被 創(chuàng)建/修改。
這是技術(shù)上的限制。在 upgradeneedHandler 之外,可以添加/刪除/更新數(shù)據(jù),但是只能在版本更新期間創(chuàng)建/刪除/更改對(duì)象庫(kù)。
要進(jìn)行數(shù)據(jù)庫(kù)版本升級(jí),主要有兩種方法:
upgradeneeded
? 中,可以進(jìn)行版本比較(例如,老版本是 2,需要升級(jí)到 4),并針對(duì)每個(gè)中間版本(2 到 3,然后 3 到 4)逐步運(yùn)行每個(gè)版本的升級(jí)。db.objectStoreNames
? 的形式獲取現(xiàn)有對(duì)象庫(kù)的列表。該對(duì)象是一個(gè) DOMStringList 提供 ?contains(name)
? 方法來(lái)檢查 name 是否存在,再根據(jù)存在和不存在的內(nèi)容進(jìn)行更新。對(duì)于小型數(shù)據(jù)庫(kù),第二種方法可能更簡(jiǎn)單。
下面是第二種方法的演示:
let openRequest = indexedDB.open("db", 2);
// 創(chuàng)建/升級(jí) 數(shù)據(jù)庫(kù)而無(wú)需版本檢查
openRequest.onupgradeneeded = function() {
let db = openRequest.result;
if (!db.objectStoreNames.contains('books')) { // 如果沒(méi)有 “books” 數(shù)據(jù)
db.createObjectStore('books', {keyPath: 'id'}); // 創(chuàng)造它
}
};
刪除對(duì)象庫(kù):
db.deleteObjectStore('books')
術(shù)語(yǔ)“事務(wù)(transaction)”是通用的,許多數(shù)據(jù)庫(kù)中都有用到。
事務(wù)是一組操作,要么全部成功,要么全部失敗。
例如,當(dāng)一個(gè)人買(mǎi)東西時(shí),我們需要:
如果完成了第一個(gè)操作,但是出了問(wèn)題,比如停電。這時(shí)無(wú)法完成第二個(gè)操作,這非常糟糕。兩件時(shí)應(yīng)該要么都成功(購(gòu)買(mǎi)完成,好?。┗蛲瑫r(shí)失?。ㄟ@個(gè)人保留了錢(qián),可以重新嘗試)。
事務(wù)可以保證同時(shí)完成。
所有數(shù)據(jù)操作都必須在 IndexedDB 中的事務(wù)內(nèi)進(jìn)行。
啟動(dòng)事務(wù):
db.transaction(store[, type]);
store
? 是事務(wù)要訪問(wèn)的庫(kù)名稱,例如 ?"books"
?。如果我們要訪問(wèn)多個(gè)庫(kù),則是庫(kù)名稱的數(shù)組。type
? – 事務(wù)類型,以下類型之一:readonly
? —— 只讀,默認(rèn)值。readwrite
? —— 只能讀取和寫(xiě)入數(shù)據(jù),而不能 創(chuàng)建/刪除/更改 對(duì)象庫(kù)。還有 versionchange
事務(wù)類型:這種事務(wù)可以做任何事情,但不能被手動(dòng)創(chuàng)建。IndexedDB 在打開(kāi)數(shù)據(jù)庫(kù)時(shí),會(huì)自動(dòng)為 upgradeneeded
處理程序創(chuàng)建 versionchange
事務(wù)。這就是它為什么可以更新數(shù)據(jù)庫(kù)結(jié)構(gòu)、創(chuàng)建/刪除 對(duì)象庫(kù)的原因。
為什么會(huì)有不同類型的事務(wù)?
性能是事務(wù)需要標(biāo)記為
readonly
和readwrite
的原因。
許多
readonly
事務(wù)能夠同時(shí)訪問(wèn)同一存儲(chǔ)區(qū),但readwrite
事務(wù)不能。因?yàn)?nbsp;readwrite
事務(wù)會(huì)“鎖定”存儲(chǔ)區(qū)進(jìn)行寫(xiě)操作。下一個(gè)事務(wù)必須等待前一個(gè)事務(wù)完成,才能訪問(wèn)相同的存儲(chǔ)區(qū)。
創(chuàng)建事務(wù)后,我們可以將項(xiàng)目添加到庫(kù),就像這樣:
let transaction = db.transaction("books", "readwrite"); // (1)
// 獲取對(duì)象庫(kù)進(jìn)行操作
let books = transaction.objectStore("books"); // (2)
let book = {
id: 'js',
price: 10,
created: new Date()
};
let request = books.add(book); // (3)
request.onsuccess = function() { // (4)
console.log("Book added to the store", request.result);
};
request.onerror = function() {
console.log("Error", request.error);
};
基本有四個(gè)步驟:
transaction.objectStore(name)
?,在(2)中獲取存儲(chǔ)對(duì)象。books.add(book)
? 的請(qǐng)求。對(duì)象庫(kù)支持兩種存儲(chǔ)值的方法:
value
? 添加到存儲(chǔ)區(qū)。僅當(dāng)對(duì)象庫(kù)沒(méi)有 ?keyPath
? 或 ?autoIncrement
? 時(shí),才提供 ?key
?。如果已經(jīng)存在具有相同鍵的值,則將替換該值。put
? 相同,但是如果已經(jīng)有一個(gè)值具有相同的鍵,則請(qǐng)求失敗,并生成一個(gè)名為 ?"ConstraInterror"
? 的錯(cuò)誤。與打開(kāi)數(shù)據(jù)庫(kù)類似,我們可以發(fā)送一個(gè)請(qǐng)求:books.add(book)
,然后等待 success/error
事件。
add
? 的 ?request.result
? 是新對(duì)象的鍵。request.error
?(如果有的話)中。在上面的示例中,我們啟動(dòng)了事務(wù)并發(fā)出了 add
請(qǐng)求。但正如前面提到的,一個(gè)事務(wù)可能有多個(gè)相關(guān)的請(qǐng)求,這些請(qǐng)求必須全部成功或全部失敗。那么我們?nèi)绾螌⑹聞?wù)標(biāo)記為已完成,并不再請(qǐng)求呢?
簡(jiǎn)短的回答是:沒(méi)有。
在下一個(gè)版本 3.0 規(guī)范中,可能會(huì)有一種手動(dòng)方式來(lái)完成事務(wù),但目前在 2.0 中還沒(méi)有。
當(dāng)所有事務(wù)的請(qǐng)求完成,并且 微任務(wù)隊(duì)列 為空時(shí),它將自動(dòng)提交。
通常,我們可以假設(shè)事務(wù)在其所有請(qǐng)求完成時(shí)提交,并且當(dāng)前代碼完成。
因此,在上面的示例中,不需要任何特殊調(diào)用即可完成事務(wù)。
事務(wù)自動(dòng)提交原則有一個(gè)重要的副作用。不能在事務(wù)中間插入 fetch
, setTimeout
等異步操作。IndexedDB 不會(huì)讓事務(wù)等待這些操作完成。
在下面的代碼中,request2
中的行 (*)
失敗,因?yàn)槭聞?wù)已經(jīng)提交,不能在其中發(fā)出任何請(qǐng)求:
let request1 = books.add(book);
request1.onsuccess = function() {
fetch('/').then(response => {
let request2 = books.add(anotherBook); // (*)
request2.onerror = function() {
console.log(request2.error.name); // TransactionInactiveError
};
});
};
這是因?yàn)?nbsp;fetch
是一個(gè)異步操作,一個(gè)宏任務(wù)。事務(wù)在瀏覽器開(kāi)始執(zhí)行宏任務(wù)之前關(guān)閉。
IndexedDB 規(guī)范的作者認(rèn)為事務(wù)應(yīng)該是短期的。主要是性能原因。
值得注意的是,readwrite
事務(wù)將存儲(chǔ)“鎖定”以進(jìn)行寫(xiě)入。因此,如果應(yīng)用程序的一部分啟動(dòng)了 books
對(duì)象庫(kù)上的 readwrite
操作,那么希望執(zhí)行相同操作的另一部分必須等待新事務(wù)“掛起”,直到第一個(gè)事務(wù)完成。如果事務(wù)處理需要很長(zhǎng)時(shí)間,將會(huì)導(dǎo)致奇怪的延遲。
那么,該怎么辦?
在上面的示例中,我們可以在新請(qǐng)求 (*)
之前創(chuàng)建一個(gè)新的 db.transaction
。
如果需要在一個(gè)事務(wù)中把所有操作保持一致,更好的做法是將 IndexedDB 事務(wù)和“其他”異步內(nèi)容分開(kāi)。
首先,執(zhí)行 fetch
,并根據(jù)需要準(zhǔn)備數(shù)據(jù)。然后創(chuàng)建事務(wù)并執(zhí)行所有數(shù)據(jù)庫(kù)請(qǐng)求,然后就正常了。
為了檢測(cè)到成功完成的時(shí)刻,我們可以監(jiān)聽(tīng) transaction.oncomplete
事件:
let transaction = db.transaction("books", "readwrite");
// ……執(zhí)行操作……
transaction.oncomplete = function() {
console.log("Transaction is complete"); // 事務(wù)執(zhí)行完成
};
只有 complete
才能保證將事務(wù)作為一個(gè)整體保存。個(gè)別請(qǐng)求可能會(huì)成功,但最終的寫(xiě)入操作可能會(huì)出錯(cuò)(例如 I/O 錯(cuò)誤或其他錯(cuò)誤)。
要手動(dòng)中止事務(wù),請(qǐng)調(diào)用:
transaction.abort();
取消請(qǐng)求里所做的所有修改,并觸發(fā) transaction.onabort
事件。
寫(xiě)入請(qǐng)求可能會(huì)失敗。
這是意料之中的事,不僅是我們可能會(huì)犯的粗心失誤,還有與事務(wù)本身相關(guān)的其他原因。例如超過(guò)了存儲(chǔ)配額。因此,必須做好請(qǐng)求失敗的處理。
失敗的請(qǐng)求將自動(dòng)中止事務(wù),并取消所有的更改。
在一些情況下,我們會(huì)想自己去處理失敗事務(wù)(例如嘗試另一個(gè)請(qǐng)求)并讓它繼續(xù)執(zhí)行,而不是取消現(xiàn)有的更改??梢哉{(diào)用 request.onerror
處理程序,在其中調(diào)用 event.preventDefault()
防止事務(wù)中止。
在下面的示例中,添加了一本新書(shū),鍵 (id
) 與現(xiàn)有的書(shū)相同。store.add
方法生成一個(gè) "ConstraInterror"
。可以在不取消事務(wù)的情況下進(jìn)行處理:
let transaction = db.transaction("books", "readwrite");
let book = { id: 'js', price: 10 };
let request = transaction.objectStore("books").add(book);
request.onerror = function(event) {
// 有相同 id 的對(duì)象存在時(shí),發(fā)生 ConstraintError
if (request.error.name == "ConstraintError") {
console.log("Book with such id already exists"); // 處理錯(cuò)誤
event.preventDefault(); // 不要中止事務(wù)
// 這個(gè) book 用另一個(gè)鍵?
} else {
// 意外錯(cuò)誤,無(wú)法處理
// 事務(wù)將中止
}
};
transaction.onabort = function() {
console.log("Error", transaction.error);
};
每個(gè)請(qǐng)求都需要調(diào)用 onerror/onsuccess ?并不,可以使用事件委托來(lái)代替。
IndexedDB 事件冒泡:請(qǐng)求 → 事務(wù) → 數(shù)據(jù)庫(kù)。
所有事件都是 DOM 事件,有捕獲和冒泡,但通常只使用冒泡階段。
因此,出于報(bào)告或其他原因,我們可以使用 db.onerror
處理程序捕獲所有錯(cuò)誤:
db.onerror = function(event) {
let request = event.target; // 導(dǎo)致錯(cuò)誤的請(qǐng)求
console.log("Error", request.error);
};
……但是錯(cuò)誤被完全處理了呢?這種情況不應(yīng)該被報(bào)告。
我們可以通過(guò)在 request.onerror
中使用 event.stopPropagation()
來(lái)停止冒泡,從而停止 db.onerror
事件。
request.onerror = function(event) {
if (request.error.name == "ConstraintError") {
console.log("Book with such id already exists"); // 處理錯(cuò)誤
event.preventDefault(); // 不要中止事務(wù)
event.stopPropagation(); // 不要讓錯(cuò)誤冒泡, 停止它的傳播
} else {
// 什么都不做
// 事務(wù)將中止
// 我們可以解決 transaction.onabort 中的錯(cuò)誤
}
};
對(duì)象庫(kù)有兩種主要的搜索類型:
book.id
? 的值或值的范圍。book.price
?。這需要一個(gè)額外的數(shù)據(jù)結(jié)構(gòu),名為“索引(index)”。首先,讓我們來(lái)處理第一種類型的搜索:按鍵。
支持精確的鍵值和被稱為“值范圍”的搜索方法 —— IDBKeyRange 對(duì)象,指定一個(gè)可接受的“鍵值范圍”。
IDBKeyRange
對(duì)象是通過(guò)下列調(diào)用創(chuàng)建的:
IDBKeyRange.lowerBound(lower, [open])
? 表示:?≥lower
?(如果 ?open
? 是 true,表示 ?>lower
?)IDBKeyRange.upperBound(upper, [open])
? 表示:≤upper(如果 ?open
? 是 true,表示 ?<upper
?)IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen])
? 表示: 在 ?lower
? 和 ?upper
? 之間。如果 open 為 true,則相應(yīng)的鍵不包括在范圍中。IDBKeyRange.only(key)
? —— 僅包含一個(gè)鍵的范圍 ?key
?,很少使用。我們很快就會(huì)看到使用它們的實(shí)際示例。
要進(jìn)行實(shí)際的搜索,有以下方法。它們接受一個(gè)可以是精確鍵值或鍵值范圍的 query
參數(shù):
store.get(query)
? —— 按鍵或范圍搜索第一個(gè)值。store.getAll([query], [count])
? —— 搜索所有值。如果 ?count
? 給定,則按 ?count
? 進(jìn)行限制。store.getKey(query)
? —— 搜索滿足查詢的第一個(gè)鍵,通常是一個(gè)范圍。store.getAllKeys([query], [count])
? —— 搜索滿足查詢的所有鍵,通常是一個(gè)范圍。如果 ?count
? 給定,則最多為 count。store.count([query])
? —— 獲取滿足查詢的鍵的總數(shù),通常是一個(gè)范圍。例如,我們存儲(chǔ)區(qū)里有很多書(shū)。因?yàn)?nbsp;id
字段是鍵,因此所有方法都可以按 id
進(jìn)行搜索。
請(qǐng)求示例:
// 獲取一本書(shū)
books.get('js')
// 獲取 'css' <= id <= 'html' 的書(shū)
books.getAll(IDBKeyRange.bound('css', 'html'))
// 獲取 id < 'html' 的書(shū)
books.getAll(IDBKeyRange.upperBound('html', true))
// 獲取所有書(shū)
books.getAll()
// 獲取所有 id > 'js' 的鍵
books.getAllKeys(IDBKeyRange.lowerBound('js', true))
對(duì)象中對(duì)值的存儲(chǔ)始終是有序的
對(duì)象內(nèi)部存儲(chǔ)的值是按鍵對(duì)值進(jìn)行排序的。
因此,請(qǐng)求的返回值,是按照鍵的順序排列的。
要根據(jù)其他對(duì)象字段進(jìn)行搜索,我們需要?jiǎng)?chuàng)建一個(gè)名為“索引(index)”的附加數(shù)據(jù)結(jié)構(gòu)。
索引是存儲(chǔ)的"附加項(xiàng)",用于跟蹤給定的對(duì)象字段。對(duì)于該字段的每個(gè)值,它存儲(chǔ)有該值的對(duì)象的鍵列表。下面會(huì)有更詳細(xì)的圖片。
語(yǔ)法:
objectStore.createIndex(name, keyPath, [options]);
name
? —— 索引名稱。keyPath
? —— 索引應(yīng)該跟蹤的對(duì)象字段的路徑(我們將根據(jù)該字段進(jìn)行搜索)。option
? —— 具有以下屬性的可選對(duì)象:unique
? —— 如果為true,則存儲(chǔ)中只有一個(gè)對(duì)象在 ?keyPath
? 上具有給定值。如果我們嘗試添加重復(fù)項(xiàng),索引將生成錯(cuò)誤。multiEntry
? —— 只有 ?keypath
? 上的值是數(shù)組時(shí)才使用。這時(shí),默認(rèn)情況下,索引將默認(rèn)把整個(gè)數(shù)組視為鍵。但是如果 ?multiEntry
? 為 true,那么索引將為該數(shù)組中的每個(gè)值保留一個(gè)存儲(chǔ)對(duì)象的列表。所以數(shù)組成員成為了索引鍵。在我們的示例中,是按照 id
鍵存儲(chǔ)圖書(shū)的。
假設(shè)我們想通過(guò) price
進(jìn)行搜索。
首先,我們需要?jiǎng)?chuàng)建一個(gè)索引。它像對(duì)象庫(kù)一樣,必須在 upgradeneeded
中創(chuàng)建完成:
openRequest.onupgradeneeded = function() {
// 在 versionchange 事務(wù)中,我們必須在這里創(chuàng)建索引
let books = db.createObjectStore('books', {keyPath: 'id'});
let index = books.createIndex('price_idx', 'price');
};
price
? 字段。unique
? 選項(xiàng)。multiEntry
? 標(biāo)志。假設(shè)我們的庫(kù)存里有4本書(shū)。下面的圖片顯示了該索引 index
的確切內(nèi)容:
如上所述,每個(gè) price 值的索引(第二個(gè)參數(shù))保存具有該價(jià)格的鍵的列表。
索引自動(dòng)保持最新,所以我們不必關(guān)心它。
現(xiàn)在,當(dāng)我們想要搜索給定的價(jià)格時(shí),只需將相同的搜索方法應(yīng)用于索引:
let transaction = db.transaction("books"); // 只讀
let books = transaction.objectStore("books");
let priceIndex = books.index("price_idx");
let request = priceIndex.getAll(10);
request.onsuccess = function() {
if (request.result !== undefined) {
console.log("Books", request.result); // 價(jià)格為 10 的書(shū)的數(shù)組
} else {
console.log("No such books");
}
};
我們還可以使用 IDBKeyRange
創(chuàng)建范圍,并查找 便宜/貴 的書(shū):
// 查找價(jià)格 <=5 的書(shū)籍
let request = priceIndex.getAll(IDBKeyRange.upperBound(5));
在我們的例子中,索引是按照被跟蹤對(duì)象字段價(jià)格 price
進(jìn)行內(nèi)部排序的。所以當(dāng)我們進(jìn)行搜索時(shí),搜索結(jié)果也會(huì)按照價(jià)格排序。
delete
方法查找要由查詢刪除的值,調(diào)用格式類似于 getAll
delete(query)
? —— 通過(guò)查詢刪除匹配的值。例如:
// 刪除 id='js' 的書(shū)
books.delete('js');
如果要基于價(jià)格或其他對(duì)象字段刪除書(shū)。首先需要在索引中找到鍵,然后調(diào)用 delete
:
// 找到價(jià)格 = 5 的鑰匙
let request = priceIndex.getKey(5);
request.onsuccess = function() {
let id = request.result;
let deleteRequest = books.delete(id);
};
刪除所有內(nèi)容:
books.clear(); // 清除存儲(chǔ)。
像 getAll/getAllKeys
這樣的方法,會(huì)返回一個(gè) 鍵/值 數(shù)組。
但是一個(gè)對(duì)象庫(kù)可能很大,比可用的內(nèi)存還大。這時(shí),getAll
就無(wú)法將所有記錄作為一個(gè)數(shù)組獲取。
該怎么辦呢?
光標(biāo)提供了解決這一問(wèn)題的方法。
光標(biāo)是一種特殊的對(duì)象,它在給定查詢的情況下遍歷對(duì)象庫(kù),一次返回一個(gè)鍵/值,從而節(jié)省內(nèi)存。
由于對(duì)象庫(kù)是按鍵在內(nèi)部排序的,因此光標(biāo)按鍵順序(默認(rèn)為升序)遍歷存儲(chǔ)。
語(yǔ)法:
// 類似于 getAll,但帶有光標(biāo):
let request = store.openCursor(query, [direction]);
// 獲取鍵,而不是值(例如 getAllKeys):store.openKeyCursor
query
? 是一個(gè)鍵值或鍵值范圍,與 ?getAll
? 相同。direction
? 是一個(gè)可選參數(shù),使用順序是:"next"
? —— 默認(rèn)值,光標(biāo)從有最小索引的記錄向上移動(dòng)。"prev"
? —— 相反的順序:從有最大的索引的記錄開(kāi)始下降。"nextunique"
?,?"prevunique"
? —— 同上,但是跳過(guò)鍵相同的記錄 (僅適用于索引上的光標(biāo),例如,對(duì)于價(jià)格為 5 的書(shū),僅返回第一本)。 光標(biāo)對(duì)象的主要區(qū)別在于 request.onSuccess
多次觸發(fā):每個(gè)結(jié)果觸發(fā)一次。
這有一個(gè)如何使用光標(biāo)的例子:
let transaction = db.transaction("books");
let books = transaction.objectStore("books");
let request = books.openCursor();
// 為光標(biāo)找到的每本書(shū)調(diào)用
request.onsuccess = function() {
let cursor = request.result;
if (cursor) {
let key = cursor.key; // 書(shū)的鍵(id字段)
let value = cursor.value; // 書(shū)本對(duì)象
console.log(key, value);
cursor.continue();
} else {
console.log("No more books");
}
};
主要的光標(biāo)方法有:
advance(count)
? —— 將光標(biāo)向前移動(dòng) ?count
? 次,跳過(guò)值。continue([key])
? —— 將光標(biāo)移至匹配范圍中的下一個(gè)值(如果給定鍵,緊接鍵之后)。無(wú)論是否有更多的值匹配光標(biāo) —— 調(diào)用 onsuccess
。結(jié)果中,我們可以獲得指向下一條記錄的光標(biāo),或者 undefined
。
在上面的示例中,光標(biāo)是為對(duì)象庫(kù)創(chuàng)建的。
也可以在索引上創(chuàng)建一個(gè)光標(biāo)。索引是允許按對(duì)象字段進(jìn)行搜索的。在索引上的光標(biāo)與在對(duì)象存儲(chǔ)上的光標(biāo)完全相同 —— 它們通過(guò)一次返回一個(gè)值來(lái)節(jié)省內(nèi)存。
對(duì)于索引上的游標(biāo),cursor.key
是索引鍵(例如:價(jià)格),我們應(yīng)該使用 cursor.primaryKey
屬性作為對(duì)象的鍵:
let request = priceIdx.openCursor(IDBKeyRange.upperBound(5));
// 為每條記錄調(diào)用
request.onsuccess = function() {
let cursor = request.result;
if (cursor) {
let primaryKey = cursor.primaryKey; // 下一個(gè)對(duì)象存儲(chǔ)鍵(id 字段)
let value = cursor.value; // 下一個(gè)對(duì)象存儲(chǔ)對(duì)象(book 對(duì)象)
let key = cursor.key; // 下一個(gè)索引鍵(price)
console.log(key, value);
cursor.continue();
} else {
console.log("No more books"); // 沒(méi)有書(shū)了
}
};
將 onsuccess/onerror
添加到每個(gè)請(qǐng)求是一項(xiàng)相當(dāng)麻煩的任務(wù)。我們可以通過(guò)使用事件委托(例如,在整個(gè)事務(wù)上設(shè)置處理程序)來(lái)簡(jiǎn)化我們的工作,但是 async/await
要方便的多。
在本章,我們會(huì)進(jìn)一步使用一個(gè)輕便的承諾包裝器 https://github.com/jakearchibald/idb 。它使用 promisified IndexedDB
方法創(chuàng)建全局 idb
對(duì)象。
然后,我們可以不使用 onsuccess/onerror
,而是這樣寫(xiě):
let db = await idb.openDB('store', 1, db => {
if (db.oldVersion == 0) {
// 執(zhí)行初始化
db.createObjectStore('books', {keyPath: 'id'});
}
});
let transaction = db.transaction('books', 'readwrite');
let books = transaction.objectStore('books');
try {
await books.add(...);
await books.add(...);
await transaction.complete;
console.log('jsbook saved');
} catch(err) {
console.log('error', err.message);
}
現(xiàn)在我們有了可愛(ài)的“簡(jiǎn)單異步代碼”和「try…catch」捕獲的東西。
如果我們沒(méi)有捕獲到錯(cuò)誤,那么程序?qū)⒁恢笔。钡酵獠孔罱?nbsp;try..catch
捕獲到為止。
未捕獲的錯(cuò)誤將成為 window
對(duì)象上的“unhandled promise rejection”事件。
我們可以這樣處理這種錯(cuò)誤:
window.addEventListener('unhandledrejection', event => {
let request = event.target; // IndexedDB 本機(jī)請(qǐng)求對(duì)象
let error = event.reason; // 未處理的錯(cuò)誤對(duì)象,與 request.error 相同
// ……報(bào)告錯(cuò)誤……
});
我們都知道,瀏覽器一旦執(zhí)行完成當(dāng)前的代碼和 微任務(wù) 之后,事務(wù)就會(huì)自動(dòng)提交。因此,如果我們?cè)谑聞?wù)中間放置一個(gè)類似 fetch
的宏任務(wù),事務(wù)只是會(huì)自動(dòng)提交,而不會(huì)等待它執(zhí)行完成。因此,下一個(gè)請(qǐng)求會(huì)失敗。
對(duì)于 promise 包裝器和 async/await
,情況是相同的。
這是在事務(wù)中間進(jìn)行 fetch
的示例:
let transaction = db.transaction("inventory", "readwrite");
let inventory = transaction.objectStore("inventory");
await inventory.add({ id: 'js', price: 10, created: new Date() });
await fetch(...); // (*)
await inventory.add({ id: 'js', price: 10, created: new Date() }); // 錯(cuò)誤
fetch
(*)
后的下一個(gè) inventory.add
失敗,出現(xiàn)“非活動(dòng)事務(wù)”錯(cuò)誤,因?yàn)檫@時(shí)事務(wù)已經(jīng)被提交并且關(guān)閉了。
解決方法與使用本機(jī) IndexedDB 時(shí)相同:進(jìn)行新事務(wù),或者將事情分開(kāi)。
在內(nèi)部,包裝器執(zhí)行本機(jī) IndexedDB 請(qǐng)求,并添加 onerror/onsuccess
方法,并返回 rejects/resolves 結(jié)果的 promise。
在大多數(shù)情況下都可以運(yùn)行, 示例在這 https://github.com/jakearchibald/idb。
極少數(shù)情況下,我們需要原始的 request
對(duì)象??梢詫?nbsp;promise
的 promise.request
屬性,當(dāng)作原始對(duì)象進(jìn)行訪問(wèn):
let promise = books.add(book); // 獲取 promise 對(duì)象(不要 await 結(jié)果)
let request = promise.request; // 本地請(qǐng)求對(duì)象
let transaction = request.transaction; // 本地事務(wù)對(duì)象
// ……做些本地的 IndexedDB 的處理……
let result = await promise; // 如果仍然需要
IndexedDB 可以被認(rèn)為是“l(fā)ocalStorage on steroids”。這是一個(gè)簡(jiǎn)單的鍵值對(duì)數(shù)據(jù)庫(kù),功能強(qiáng)大到足以支持離線應(yīng)用,而且用起來(lái)比較簡(jiǎn)單。
最好的指南是官方文檔。目前的版本 是2.0,但是 3.0 版本中的一些方法(差別不大)也得到部分支持。
基本用法可以用幾個(gè)短語(yǔ)來(lái)描述:
idb.openDb(name, version, onupgradeneeded)
?onupgradeneeded
? 處理程序中創(chuàng)建對(duì)象存儲(chǔ)和索引,或者根據(jù)需要執(zhí)行版本更新。db.transaction('books')
?(如果需要的話,設(shè)置 readwrite)。transaction.objectStore('books')
?。這里有一個(gè)小應(yīng)用程序示例:
更多建議: