国产chinesehdxxxx野外,国产av无码专区亚洲av琪琪,播放男人添女人下边视频,成人国产精品一区二区免费看,chinese丰满人妻videos

Javascript IndexedDB

2023-02-17 10:57 更新

IndexedDB 是一個(gè)瀏覽器內(nèi)建的數(shù)據(jù)庫(kù),它比 ?localStorage? 強(qiáng)大得多。

  • 通過(guò)支持多種類型的鍵,來(lái)存儲(chǔ)幾乎可以是任何類型的值。
  • 支撐事務(wù)的可靠性。
  • 支持鍵值范圍查詢、索引。
  • 和 ?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ǔ)。

打開(kāi)數(shù)據(jù)庫(kù)

要想使用 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)題。

并行更新問(wèn)題

提到版本控制,有一個(gè)相關(guān)的小問(wèn)題。

舉個(gè)例子:

  1. 一個(gè)用戶在一個(gè)瀏覽器標(biāo)簽頁(yè)中打開(kāi)了數(shù)據(jù)庫(kù)版本為 ?1? 的我們的網(wǎng)站。
  2. 接下來(lái)我們發(fā)布了一個(gè)更新,使得代碼更新了。
  3. 接下來(lái)同一個(gè)用戶在另一個(gè)瀏覽器標(biāo)簽中打開(kāi)了這個(gè)網(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ō),在這我們做兩件事:

  1. 如果當(dāng)前數(shù)據(jù)庫(kù)版本過(guò)時(shí),?db.onversionchange? 監(jiān)聽(tīng)器會(huì)通知我們并行嘗試更新。
  2. ?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)。

對(duì)象庫(kù)(object store)

要在 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í),主要有兩種方法:

  1. 我們實(shí)現(xiàn)每個(gè)版本的升級(jí)功能:從 1 到 2,從 2 到 3,從 3 到 4,等等。在 ?upgradeneeded? 中,可以進(jìn)行版本比較(例如,老版本是 2,需要升級(jí)到 4),并針對(duì)每個(gè)中間版本(2 到 3,然后 3 到 4)逐步運(yùn)行每個(gè)版本的升級(jí)。
  2. 或者我們可以檢查數(shù)據(jù)庫(kù):以 ?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')

事務(wù)(transaction)

術(shù)語(yǔ)“事務(wù)(transaction)”是通用的,許多數(shù)據(jù)庫(kù)中都有用到。

事務(wù)是一組操作,要么全部成功,要么全部失敗。

例如,當(dāng)一個(gè)人買(mǎi)東西時(shí),我們需要:

  1. 從他們的賬戶中扣除這筆錢(qián)。
  2. 將該項(xiàng)目添加到他們的清單中。

如果完成了第一個(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è)步驟:

  1. 創(chuàng)建一個(gè)事務(wù),在(1)表明要訪問(wèn)的所有存儲(chǔ)。
  2. 使用 ?transaction.objectStore(name)?,在(2)中獲取存儲(chǔ)對(duì)象。
  3. 在(3)執(zhí)行對(duì)對(duì)象庫(kù) ?books.add(book)? 的請(qǐng)求。
  4. ……處理請(qǐng)求 成功/錯(cuò)誤(4),還可以根據(jù)需要發(fā)出其他請(qǐng)求。

對(duì)象庫(kù)支持兩種存儲(chǔ)值的方法:

  • put(value, [key]) 將 ?value? 添加到存儲(chǔ)區(qū)。僅當(dāng)對(duì)象庫(kù)沒(méi)有 ?keyPath? 或 ?autoIncrement? 時(shí),才提供 ?key?。如果已經(jīng)存在具有相同鍵的值,則將替換該值。
  • add(value, [key]) 與 ?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ì)象的鍵。
  • 錯(cuò)誤在 ?request.error?(如果有的話)中。

事務(wù)的自動(dòng)提交

在上面的示例中,我們啟動(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ù)中間插入 fetchsetTimeout 等異步操作。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 事件。

錯(cuò)誤處理

寫(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ù)有兩種主要的搜索類型:

  1. 通過(guò)鍵值或鍵值范圍。在我們的 “books” 存儲(chǔ)中,將是 ?book.id? 的值或值的范圍。
  2. 通過(guò)另一個(gè)對(duì)象字段,例如 ?book.price?。這需要一個(gè)額外的數(shù)據(jù)結(jié)構(gòu),名為“索引(index)”。

通過(guò) key 搜索

首先,讓我們來(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)求的返回值,是按照鍵的順序排列的。

通過(guò)使用索引的字段搜索

要根據(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? 字段。
  • 價(jià)格不是唯一的,可能有多本書(shū)價(jià)格相同,所以我們不設(shè)置唯一 ?unique? 選項(xiàng)。
  • 價(jià)格不是一個(gè)數(shù)組,因此不適用多入口 ?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à)格排序。

從存儲(chǔ)中刪除

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ǔ)。

光標(biāo)(Cursors)

像 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ū)了
  }
};

Promise 包裝器

將 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」捕獲的東西。

錯(cuò)誤處理

如果我們沒(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ò)誤……
});

“非活躍事務(wù)”陷阱

我們都知道,瀏覽器一旦執(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)。

  1. 準(zhǔn)備數(shù)據(jù),先獲取所有需要的信息。
  2. 然后保存在數(shù)據(jù)庫(kù)中。

獲取本機(jī)對(duì)象

在內(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; // 如果仍然需要

總結(jié)

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)描述:

  1. 獲取一個(gè) promise 包裝器,比如 idb
  2. 打開(kāi)一個(gè)數(shù)據(jù)庫(kù):?idb.openDb(name, version, onupgradeneeded)?
    • 在 ?onupgradeneeded? 處理程序中創(chuàng)建對(duì)象存儲(chǔ)和索引,或者根據(jù)需要執(zhí)行版本更新。
  3. 對(duì)于請(qǐng)求:
    • 創(chuàng)建事務(wù) ?db.transaction('books')?(如果需要的話,設(shè)置 readwrite)。
    • 獲取對(duì)象存儲(chǔ) ?transaction.objectStore('books')?。
  4. 按鍵搜索,可以直接調(diào)用對(duì)象庫(kù)上的方法。
    • 要按對(duì)象字段搜索,需要?jiǎng)?chuàng)建索引。
  5. 如果內(nèi)存中容納不下數(shù)據(jù),請(qǐng)使用光標(biāo)。

這里有一個(gè)小應(yīng)用程序示例:

完整示例


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)