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