小議IndexedDB中的主要對象

IndexedDB是在客戶端的瀏覽器裏存儲大量數據的一個方案,網易雲信的IM也使用了IndexedDB來存儲客戶的本地消息,並且隨着PWA的興起,學會使用IndexedDB是必不可少的。本文主要將IndexedDB裏的一些主要概念抽解出來,幫助你們鞏固。javascript

好~ 你們能夠把瀏覽器的開發者工具打開,測試一下下面的示例。java

IDBFactory對象

window.indexedDB 這個是在全局環境能夠訪問到的一個IndexedDB的工廠對象,在Console裏輸入 indexedDB 而後回車,就能夠看到這個對象是 IDBFactory 類。相似的, localStorage 是 Storage 類的實例對象,   caches  是 CacheStorage 類的一個實例對象。sql

在console裏展開這個IDBFactory,展開 __proto__ ,就能夠看到indexedDB這個對象的原型上有哪些方法了。
數據庫

image.png

咱們能夠經過 var request = indexDB.open("dbName", versionCode) 來打開一個存儲對象來存儲數據。數組

IDBRequest對象

這個對象是indexdDB中對全部異步操做的請求對象,類比XMLHttpRequest對象,對請求的結果都須要監聽success事件。後文中凡是綁定onsuccess函數的都是在IDBRequest對象上進行,全部的返回結果都在該對象的result屬性上,因此能夠不用event.target.result來引用結果值,類比xhr.responseText屬性。
request.onerror/onupgradeneeded/onsuccess = function(event){}瀏覽器

request.onerror/onupgradeneeded/onsuccess = function(event){
	db = event.target.result; // event.target.result === resust.result
  // 這裏的db就是咱們的數據庫對象
}
複製代碼
  1. onupgradeneeded是在建立數據庫 或者 數據庫version比原來高的時候會觸發.在此函數內部進行一些數據庫的初始化,例如創建索引和主鍵.
  2. IDBFactory.open 返回的是IDBOpenDBRequest ,繼承自IDBRequest , 只有 IDBOpenDBRequest 上纔會有 onupgradeneeded 事件回調.
  3. onupgradeneeded 是咱們惟一能夠修改數據庫結構的地方。在這裏面,咱們能夠建立和刪除對象存儲空間以及構建和刪除索引.
  4. 執行完onupgredeneeded後將繼續觸發onsuccess事件.

IDBDatabase對象

在上面咱們調用open方法後,在request.result上能夠獲取到一個IDBDatabase對象,依然在console裏展開這個對象,以下圖。
數據結構

image.png

能夠看到db的原型上存在createObjectStore和deleteObjectStore等方法,ObjectStore至關於SQL裏的table,MongoDB裏的Collection.

var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
複製代碼
  1. 建立一個名爲 customers 的對象存儲空間,主鍵是ssn; 第一個參數是名稱 ,第二個參數是配置參數.
  2. 建立一個已存在的對象存儲空間或者刪除一個不存在的對象存儲空間 都會拋出異常.

IDBObjectStore對象

再展開上面的objectStore對象,能夠看到下圖所示ObjectStore對象上的變量和方法。
異步

image.png

  • objectStore.creatIndex
objectStore.createIndex("indexName", "keyPath", { unique: false });
複製代碼
  1. 對象存儲空間上新增索引。
  2. 建立並返回新的IDBIndex對象,該方法只能從versionchange事務模式的回調方法中被調用。
  3. 若存儲的對象沒有該屬性,將不會出如今該索引的集合裏。
  • objectStore.add(Object) —— 對象存儲空間中新增對象.
  • objectStore.put(Object [, key] ) —— 更新對象
  • objectStore.get/delete —— 根據key來查找對象
  • objectStore.count(keyOrKeyRange) —— 若是傳入null,返回0;傳入其餘值,返回共享該key值或者key range的object的數量.

到目前爲止,咱們能夠知道如何在建立數據庫或者更新數據庫時,對objectStore上的數據進行變動了。下面用代碼展現一下剛纔介紹的各類對象及操做方法。函數

const dbVersion = 1;// 默認的db版本號是0,若是你要傳入新的數據庫,那麼版本號必須大於0;
const dbName = 'test'

let db;
// IDBOpenReqeust對象
const openDBReq = window.indexedDB.open(dbName, dbVersion)
openDBReq.onerror = event =>  console.error(event)
// 建立數據庫或者數據庫版本變動時會觸發
openDBReq.onupgradeneeded = (event) => {
	
  // 若是是數據庫版本變動,會有版本號
  const { newVersion, oldVersion } = event;
  
  // IDBDatabse對象
  db = event.target.result;
  // 在這個函數裏,咱們須要建立一個叫customer的表
  let objectStore = db.createObjectStore('customers',{keyPath:'ssn'})
  // 建立一個索引,便於快速查找數據
  objectStore.createIndex(":)indexName", "name", { unique: false });
  
  // 能夠在初始化的時候放入一些初始數據
  let reqPut = objectStore.put({ssn:123,name:"yunxinim"}) 
  reqPut.onerror = e => console.error('初始化時放入數據失敗')
  reqPut.onsuccess = e => console.log('放置初始化數據成功')
  
}
openDBReq.onsuccess = event => db = event.target.result;
// 若是數據庫不使用了,能夠調用 openDBReq.close()關閉數據庫


複製代碼

那麼,在openDBReq.onsuccess後又怎樣操做objectStore呢? 咱們能夠調用IDBDatabse對象上 transaction 方法來操做ObjectStore,該方法會返回一個IDBTransaction對象。工具

IDBTransaction對象

在數據庫裏有個概念叫事務,一個事務執行過程當中若是錯誤,那麼在這個事務執行的過程當中所作的操做都會失效,這樣保證代碼執行到一半出錯時,不會對原來的數據形成影響。

var transaction = db.transaction(['customers', 'orders'],'readwrite');
複製代碼
  • transaction() 方法接受三個參數(雖然兩個是可選的)並返回一個事務對象。
  • 第一個參數是事務但願跨越的對象存儲空間的列表。若是你但願事務可以跨越全部的對象存儲空間你能夠傳入一個空數組。
  • 若是你沒有爲第二個參數指定任何內容,你獲得的是隻讀事務。由於這裏咱們是想要寫入因此咱們須要傳入 "readwrite" 標識。


你懂的,在console裏展開一下transaction,看看有啥屬性和方法。

image.png

事務的生命週期

事務和事件循環隊列的聯繫很是密切。若是你建立了一個事務可是並無使用它就返回了,那麼這個事務將會在下一個事件循環的時候就過時了,也就是說須要從新建立事務。保持事務活躍的惟一方法就是在其上構建一個請求(IDBRequest),這個請求會加入到事件循環隊列裏,這個事務也就保活了。當請求完成時(onsuccess)你將會獲得一個 DOM 事件,你能夠選擇在這個事件回調裏構建新的請求來延長事務。若是你沒有延長事務就返回到了事件循環,那麼事務將會過去,依此類推。只要還有待處理的請求,事務就會保持活躍。事務生命週期真的很簡單可是可能須要一點時間你才能對它變得習慣。若是你開始看到 TRANSACTION_INACTIVE_ERR 錯誤代碼,說明這個生命週期沒有續上。

PS: 當你要操做多個ObjectStore時,最好使用transaction來保證在失敗的狀況下不會修改數據。
PSS: onupgradeneeded事件裏( event.target 或 openDBReq)上存在一個屬性transaction,其實這是一個 versionchange 的事務,在數據庫版本變動的時候,若是你須要對舊版本數據進行遷移,那麼你可能會須要使用這個事務來延長 onupgradeneeded 時間.

下面用代碼來展現一下如何使用事務來操做ObjectStore

const dbVersion = 1;// 默認的db版本號是0,若是你要傳入新的數據庫,那麼版本號必須大於0;
const dbName = 'test'

let db;
// IDBOpenReqeust對象
const openDBReq = window.indexedDB.open(dbName, dbVersion)
openDBReq.onerror = event =>  console.error(event)
// 建立數據庫或者數據庫版本變動時會觸發
openDBReq.onupgradeneeded = (event) => {
	
  // 若是是數據庫版本變動,會有版本號
  const { newVersion, oldVersion } = event;
  
  // IDBDatabse對象
  db = event.target.result;
  // 在這個函數裏,咱們須要建立一個叫customer的表
  let objectStore = db.createObjectStore('customers',{keyPath:'ssn'})
  // 建立一個索引,便於快速查找數據
  objectStore.createIndex(":)indexName", "name", { unique: false });
  
  // 能夠在初始化的時候放入一些初始數據
  let reqPut = objectStore.put({ssn:123,name:"yunxinim"}) 
  reqPut.onerror = e => console.error('初始化時放入數據失敗')
  reqPut.onsuccess = e => console.log('放置初始化數據成功')
  
}
openDBReq.onsuccess = event => {
	db = event.target.result;
  
  // 建立一個讀寫customers裏的事務
  const tx = db.transaction(['customers'],'readwrite');
  let customers = tx.objectStore('customers')
  customers.get(123).onsuccess = evt => {
  	let theCustomer = evt.target.result;
    console.log('獲取數據', theCustomer)
    theCustomer.phone = theCustomer.phone + 130;
    customers.put(theCustomer).onsuccess = evt => {
    	console.log('更新數據成功', theCustomer)
    }
  }
}


複製代碼

IDBCursor對象

咱們可使用遊標來遍歷ObjectStore裏的全部數據。

objectStore.openCursor().onsuccess = function(e){
    var cursor = e.target.result;
    if (cursor) {
        alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
        cursor.continue();
    }
    else {
         alert("No more entries!");
    }
}
複製代碼

openCursor() 函數須要幾個參數。首先,你可使用一個 key range對象來限制被檢索的項目的範圍。第二,你能夠指定你但願進行迭代的方向。在上面的示例中,咱們在以升序迭代全部的對象。

// IDBCursorWithValue 繼承自IDBCursor
{
    direction:"next",
    key:"444-44-4444",
    primaryKey:"444-44-4444",
    source: IDBObjectStore,
    value: Object
}
複製代碼

image.png

e.target.result是IDBCursorWithValue 對象,該對象中的key是key,value是key對應的對象;

若是要獲得全部的objectStore中的對象,可使用objectStore.getAll() .(ps:getAll方法可能有兼容性問題)

keyRage至關於sql裏的whereClause.

IDBIndex對象

IDBIndex對象是索引,在建立ObjectStore時能夠爲一個屬性建立一個索引,便於快速查找。
使用索引: objectStore.index("indexName")

var index = objectStore.index(":)name");
// 這裏要使用在onupgradeneeded裏建立的索引的名稱,不是對象的name屬性名稱
index.get("yunxinim").onsuccess = function(event) {
  alert("Donna's primaryKey is " + event.target.result.primaryKey);
};
複製代碼

image.png

name遊標不是惟一的,所以 name被設成  Donna的記錄可能不止一條。在這種狀況下,你老是獲得鍵值最小的那個(PS:直接在objectStore上調用get,若是同一個key有多個條記錄,那麼只會返回一條記錄)。若是你須要訪問帶有給定  name的全部的記錄你可使用一個遊標。你能夠在索引上打開兩個不一樣類型的遊標。一個常規遊標映射索引屬性到對象存儲空間中的對象。一個鍵索引映射索引屬性到用來存儲對象存儲空間中的對象的鍵。objectStore的遊標和索引的遊標的不一樣之處被展現以下:

objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key 是一個 primaryKey,即createObjectStore時指定的keyPath屬性對應的值, 就像 "Bill", 
    // 而後 cursor.value 是整個對象。
    alert("primary key: " + cursor.key + ", SSN: " + cursor.value.ssn + ", name: " + cursor.value.name);
    cursor.continue();
  }
};
index.openKeyCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is 一個 當前索引的值,若是索引的是對象上的name,那麼cursor.key就是name,
    // primaryKey是createObjectStore時指定的keyPath屬性對應的值,
    // cursor.value 沒有值。
    // 在這個cursor上沒有辦法能夠獲得存儲對象的其他部分。
    alert("Name: " + cursor.key + ", value is undefined: " + cursor.value);
    cursor.continue();
  }
};
複製代碼

若是要從遊標上跳到指定次數的遊標處,可使用cursor.advance(Number biggerThan0)方法,直接前進指定次數的遊標。傳入的參數要求必須大於0。

image.png

使用Cursor的range和direction

若是你想要限定你在遊標中看到的值的範圍,你可使用一個 key range 對象而後把它做爲第一個參數傳給 openCursor()或是 openKeyCursor()。你能夠構造一個只容許一個單一 key 的 key range,或者一個具備下限或上限,或者一個既有上限也有下限。邊界能夠是閉合的(也就是說 key range 包含給定的值)或者是「開放的」(也就是說 **key range **不包括給定的值)。這裏是它如何工做的:

// 只匹配 "Donna"
var singleKeyRange = IDBKeyRange.only("Donna");
// 匹配全部在 "Bill" 前面的, 包括 "Bill"
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");
// 匹配全部在 「Bill」 前面的, 可是不須要包括 "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);
// Match anything up to, but not including, "Donna"
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);
//Match anything between "Bill" and "Donna", but not including "Donna"
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);
index.openCursor(boundKeyRange).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};
複製代碼

有時候你可能想要以倒序而不是正序(全部遊標的默認順序)來遍歷。切換方向是經過傳遞 prev到 openCursor() 方法來實現的:

objectStore.openCursor(null, IDBCursor.prev).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};
複製代碼

一些option對象

//新建objectStore時
db.createObjectStore("name",{
    (DOMString or sequence<DOMString>)? keyPath = null;
    boolean                             autoIncrement = false;
});
//新增`index`時
dictionary IDBIndexParameters {
    boolean unique = false;
    boolean multiEntry = false;
};
//除了upgradeneeded 還有 onblocked 是在另外的標籤頁打開數據庫時觸發
//該事件包含兩個參數
{
    readonly    attribute unsigned long long  oldVersion;
    readonly    attribute unsigned long long? newVersion;
};
複製代碼

transaction打開objectStore有三個選項**readonly**,**readwrite**,**versionchange**;

類型爲**versionchange****transaction**能夠修改**index**,**keypath**,可是這種類型的**transaction**只能由**upgradeneeded**事件生成.

indexDB的方法有 open, deleteDatabase, cmp
**cmp**比較傳入的參數,若是數值出錯,將拋出異常. 第一個數比第二個數大,返回1;比第二個數小,返回-1;相同,返回0;

Database的屬性:

readonly    attribute DOMString          name;
readonly    attribute unsigned long long version;
readonly    attribute DOMStringList      objectStoreNames;
IDBObjectStore createObjectStore (DOMString name, optional IDBObjectStoreParameters optionalParameters);
void           deleteObjectStore (DOMString name);
IDBTransaction transaction ((DOMString or sequence<DOMString>) storeNames, optional IDBTransactionMode mode = "readonly");
void           close ();
複製代碼

須要注意的點

直接open一個數據庫,若是數據庫不存在,將新建該數據庫,version爲0;

思考

如何實現跨表查詢?

目前只有經過indexRange實現簡單的多條件查詢.跨表只能經過在遊標裏面循環. 建議修改本身存儲的數據結構,參考No-SQL數據庫的數據結構設計,存儲的值能夠有多層。

最後,原生的接口很難用,建議使用一些封裝好的庫,例如Dexie.js,idb等等。

參考:
[developer.mozilla.org/zh-CN/docs/…](developer.mozilla.org/zh-CN/docs/…)

相關文章
相關標籤/搜索