IndexedDB的使用教程

以下是我在學(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ì)
    得到重用。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容