小议IndexedDB中的主要对象
IndexedDB是在客戶端的瀏覽器里存儲大量數據的一個方案,網易云信的IM也使用了IndexedDB來存儲客戶的本地消息,而且隨著PWA的興起,學會使用IndexedDB是必不可少的。本文主要將IndexedDB里的一些主要概念抽解出來,幫助大家鞏固。
好~ 大家可以把瀏覽器的開發者工具打開,測試一下下面的示例。
IDBFactory對象
window.indexedDB?這個是在全局環境可以訪問到的一個IndexedDB的工廠對象,在Console里輸入 indexedDB?然后回車,就可以看到這個對象是 IDBFactory?類。類似的, localStorage?是 Storage?類的實例對象,? ?caches? 是 CacheStorage?類的一個實例對象。
在console里展開這個IDBFactory,展開 __proto__?,就可以看到indexedDB這個對象的原型上有哪些方法了。
我們可以通過?varrequest = indexDB.open("dbName", versionCode)?來打開一個存儲對象來存儲數據。
IDBRequest對象
這個對象是indexdDB中對所有異步操作的請求對象,類比XMLHttpRequest對象,對請求的結果都需要監聽success事件。后文中凡是綁定onsuccess函數的都是在IDBRequest對象上進行,所有的返回結果都在該對象的result屬性上,所以可以不用event.target.result來引用結果值,類比xhr.responseText屬性。
request.onerror/onupgradeneeded/onsuccess = function(event){}
onupgradeneeded是在創建數據庫或者數據庫version比原來高的時候會觸發.在此函數內部進行一些數據庫的初始化,例如建立索引和主鍵.
?IDBFactory.open?返回的是IDBOpenDBRequest?,繼承自IDBRequest?, 只有 IDBOpenDBRequest?上才會有 onupgradeneeded?事件回調.??
onupgradeneeded?是我們唯一可以修改數據庫結構的地方。在這里面,我們可以創建和刪除對象存儲空間以及構建和刪除索引.
執行完onupgredeneeded后將繼續觸發onsuccess事件.
?
IDBDatabase對象
在上面我們調用open方法后,在request.result上可以獲取到一個IDBDatabase對象,依然在console里展開這個對象,如下圖。
可以看到db的原型上存在createObjectStore和deleteObjectStore等方法,ObjectStore相當于SQL里的table,MongoDB里的Collection.
創建一個名為 customers?的對象存儲空間,主鍵是ssn; 第一個參數是名稱 ,第二個參數是配置參數.
創建一個已存在的對象存儲空間或者刪除一個不存在的對象存儲空間都會拋出異常.
IDBObjectStore對象
再展開上面的objectStore對象,可以看到下圖所示ObjectStore對象上的變量和方法。
objectStore.creatIndex
對象存儲空間上新增索引。
創建并返回新的IDBIndex對象,該方法只能從versionchange事務模式的回調方法中被調用。
若存儲的對象沒有該屬性,將不會出現在該索引的集合里。
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,看看有啥屬性和方法。
事務的生命周期
事務和事件循環隊列的聯系非常密切。如果你創建了一個事務但是并沒有使用它就返回了,那么這個事務將會在下一個事件循環的時候就過期了,也就是說需要重新創建事務。保持事務活躍的唯一方法就是在其上構建一個請求(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 moreentries!");} }openCursor()?函數需要幾個參數。首先,你可以使用一個?key range對象來限制被檢索的項目的范圍。第二,你可以指定你希望進行迭代的方向。在上面的示例中,我們在以升序迭代所有的對象。
// IDBCursorWithValue 繼承自IDBCursor {direction:"next",key:"444-44-4444",primaryKey:"444-44-4444",source: IDBObjectStore,value: Object }e.target.result是IDBCursorWithValue對象,該對象中的key是key,value是key對應的對象;
如果要得到所有的objectStore中的對象,可以使用objectStore.getAll().(ps:getAll方法可能有兼容性問題)
keyRage相當于sql里的whereClause.
IDBIndex對象
IDBIndex對象是索引,在創建ObjectStore時可以為一個屬性創建一個索引,便于快速查找。
使用索引:objectStore.index("indexName")
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。
使用Cursor的range和direction
如果你想要限定你在游標中看到的值的范圍,你可以使用一個?key range?對象然后把它作為第一個參數傳給?openCursor()或是?openKeyCursor()。你可以構造一個只允許一個單一?key?的?key range,或者一個具有下限或上限,或者一個既有上限也有下限。邊界可以是閉合的(也就是說 `key range` 包含給定的值)或者是“開放的”(也就是說?keyrange?不包括給定的值)。這里是它如何工作的:
// 只匹配 "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 notincluding "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 orsequence<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 longversion; readonly attribute DOMStringList objectStoreNames; IDBObjectStore createObjectStore (DOMString name, optionalIDBObjectStoreParameters optionalParameters); void deleteObjectStore(DOMString name); IDBTransaction transaction ((DOMString or sequence<DOMString>)storeNames, optional IDBTransactionMode mode = "readonly"); void close ();需要注意的點
直接open一個數據庫,如果數據庫不存在,將新建該數據庫,version為0;
思考
如何實現跨表查詢?
建議修改自己存儲的數據結構,參考No-SQL數據庫的數據結構設計,存儲的值可以有多層。
最后,原生的接口很難用,建議使用一些封裝好的庫,例如Dexie.js,idb等等。
參考:
[https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API](https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API)
?
總結
以上是生活随笔為你收集整理的小议IndexedDB中的主要对象的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网易云信7月大事记
- 下一篇: 云信私有化方案中如何搭建高可用的日志和监