IndexedDB簡介與入門
背景
在本地離線存儲的場景中,IndexedDB作為一個nosql的本地DB存儲一直發揮著重要的作用。在需要存儲大量數據時,IndexedDB能夠有效地滿足你的需求。下面,我將簡單介紹下IndexedDB的簡單使用方法和在我閱讀W3C規范文檔時看到的一些需要注意的細節。實例大部分參照MDN中的介紹實例,同時對整體邏輯和實例做了一些改動。
使用方法
打開數據庫
我們使用window.indexedDB.open(DBName)來打開數據庫。具體實例如下:
- const request = window.indexedDB.open(DBName);
- request.onsuccess = function(event) {
- //request === event.target;
- }
- request.onerror = function(event) {}
當數據庫連接時,會返回一個IDBOpenDBRequest對象。在連接建立成功時,會觸發onsuccess事件,其中函數參數event的target屬性就是request對象。
創建和更新數據庫版本號
window.indexedDB.open的第二個參數即為版本號。在不指定的情況下,默認版本號為1。具體實例如下:
- const request = window.indexedDB.open(DBName, 2);
在需要更新數據庫的schema(模式)時,需要更新版本號。此時我們指定一個高于之前版本的版本號,就會觸發onupgradeneeded事件。類似的,在***次創建時,也會觸發此事件。
我們需要注意的是,版本號是一個unsigned long long數字,這意味著它可以是一個非常大的整數。但是,它不能是一個小數,否則它將會被轉為最近的整數,同時有可能導致onUpgradeneeded事件不觸發(bug)。
構建數據庫
我們使用createObjectStore來創建一個存儲空間。同時,使用createIndex來創建索引。具體實例如下:
- const objectStore = db.createObjectStore('customers');
- objectStore.createIndex('name', 'name', {unique:false});
我們從***個函數createObjectStore開始說起。該函數接受兩個參數,***個參數為存儲空間的名稱,即我們上面的customers。同時,它還有第二個可選參數,keyPath指定存儲的key值為存儲對象的某個屬性,autoIncrement指定了key值是否自增(當key值為默認的從1開始到2^53的整數時)。具體實例如下:
- const objectStore = db.createObjectStore('customers',{keyPath:'id', autoIncrement:true});
第二個函數為createIndex,它的***個參數為索引的名稱,第二個參數是指定根據存儲數據的哪一個屬性來構建索引,第三個屬性unique為是否允許指定的索引值是否可以相等。具體示例如下:
- objectStore.createIndex('by_name', 'name', {unique:false});
- objectStore.createIndex('by_email', 'email', {unique:true});
當存儲空間初始化完成后,我們需要把數據放入存儲空間中,我們可以直接調用add方法將數據放入存儲空間內,具體實例如下:
- //方法1,規范文檔推薦使用, key值如果指定自增,可以不填
- objectStore.put(data, key);
- //方法2,key值同上
- objectStore.add(data, key);
事務
在IndexedDB中,我們也能夠使用事務來進行數據庫的操作。事務有三個模式:
- readOnly,只讀
- readwrite,讀寫
- versionchange,數據庫版本變化
我們創建一個事務時,需要從上面選擇一種模式,如果不指定的話,則默認為只讀模式。具體實例如下:
- const transition = db.transition(['customers'], 'readwrite');
事務函數transition的***個參數為需要關聯的存儲空間,第二個可選參數為事務模式。與上面類似,事務成功時也會觸發onsuccess函數,失敗時觸發onerror函數。
事務的操作都是原子性的。
刪除
使用delete函數即可刪除數據。具體使用方法如下:
- const request = db.transaction(['customers'], 'readwrite').objectStore('customers').delete(author);
獲取數據
get方法
最簡單的方法就是使用自帶的get函數來獲取存儲空間的值,具體實例如下:
- db.transition(['customer']).objectStore('customer').get(keyName).onuccess = function(){};
游標方法
我們使用openCursor來創建游標。
- const objectStore = db.transaction('customers').objectStore('customers');
- objectStore.openCursor().onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- alert(cursor.value.name);
- cursor.continue();
- }
- else {
- alert('No more entries!');
- }
- };
使用游標時有一個需要注意的地方,當游標便利整個存儲空間但是并未找到給定條件的值時,仍然會觸發onsuccess函數。
同時,如果你想要限定你在游標中看到的值的范圍,你可以使用一個key range對象然后把它作為***個參數傳給openCursor或是openKeyCursor方法。你可以構造一個只允許一個單一key的 key range,或者一個具有下限或上限,或者一個既有上限也有下限。邊界可以是閉合的(也就是說key range包含給定的值)或者是“開放的”(也就是說key range不包括給定的值)。具體實例如下:
- // 只匹配 'Donna'
- const singleKeyRange = IDBKeyRange.only('Donna');
- // 匹配所有在 'Bill' 前面的, 包括 'Bill'
- const lowerBoundKeyRange = IDBKeyRange.lowerBound('Bill');
- // 匹配所有在 “Bill” 前面的, 但是不需要包括 'Bill'
- const lowerBoundOpenKeyRange = IDBKeyRange.lowerBound('Bill', true);
- // Match anything up to, but not including, 'Donna'
- const upperBoundOpenKeyRange = IDBKeyRange.upperBound('Donna', true);
- //Match anything between 'Bill' and 'Donna', but not including 'Donna'
- const boundKeyRange = IDBKeyRange.bound('Bill', 'Donna', false, true);
- index.openCursor(boundKeyRange).onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- // Do something with the matches.
- cursor.continue();
- }
- };
IDBKeyRange對象的lowerBound和upperBound方法分別表示檢索指定key值前或者后的范圍(第二個參數指定邊界是否閉合,如果為true則不閉合)。
有時候你可能想要以倒序而不是正序(所有游標的默認順序)來遍歷。切換方向是通過傳遞prev到 openCursor方法來實現的。
- objectStore.openCursor(null, IDBCursor.prev).onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- // Do something with the entries.
- cursor.continue();
- }
- };
因為 “name” 索引不是***的,那就有可能存在具有相同 name 的多條記錄。要注意的是這種情況不可能發生在對象存儲空間上,因為鍵必須永遠是***的。如果你想要在游標在索引迭代過程中過濾出重復的,你可以傳遞 nextunique (或 prevunique 如果你正在向后尋找)作為方向參數。 當 nextunique 或是 prevunique 被使用時,被返回的那個總是鍵最小的記錄。
索引方法
在前面構建數據庫時,我們創建了兩個索引。現在,我們就通過這兩個索引來演示如何通過索引進行搜索。具體實例如下:
- const index = objectStore.index('by_name');
- //***種,使用get方法
- index.get(name).onsuccess = function(){
- alert(event.target.result.name);
- }
- //第二種,使用游標
- //普通游標
- index.openCursor().onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- // cursor.key 是一個 name, 就像 'Bill', 然后 cursor.value 是整個對象。
- alert('Name: ' + cursor.key + ', id' + cursor.value.id + ', email: ' + cursor.value.email);
- cursor.continue();
- }
- };
- //鍵游標
- index.openKeyCursor().onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- // cursor.key is 一個 name, 就像 'Bill', 然后 cursor.value 是那個id,即存儲對象的key值。
- alert('Name: ' + cursor.key + ', 'id: ' + cursor.value);
- cursor.continue();
- }
- };
異常處理
在瀏覽器有如下操作的情況下,indexedDB可能會出現異常:
- 用戶清除瀏覽器緩存
- 存儲空間超過大小限制
W3C規范文檔需要注意的應用點
key值能夠接受的數據類型
在IndexedDB中,key值可以接受一下幾種類型的值:
- Number
- Date
- String
- ArrayBuffer
- Array
具體說明可以參考此處。
key path能夠接受的數據類型
在IndexedDB中,key path(主鍵)能夠接接受如下數據類型:
- Blob
- File
- Array
- StringI
注:空格不能出現在key path中。
value能夠接受的數據類型
在IndexedDB中,value能夠接受ECMA-262中所有的值,例如String,Date,ImageDate等。
事務中斷后,會不會影響key值的自增
IndexedDB在沒有指定key值的時候就會采用自增的key值,不受其他任何影響。如果一個事務在中途中斷,那么key值的自增將會從事務開始前的key開始。
IndexedDB的安全相關問題
IndexedDB也受到瀏覽器同源策略的限制。
總結
IndexedDB在本地存儲中有著無可替代的作用,是替代關系型數據庫websql的產品。在許多需要運用離線存儲的場景下,IndexedDB能夠給我們提供有效的支撐。但是,IndexedDB畢竟存在于客戶端,會出現諸如用戶清理緩存(Windows下更為常見)等破壞數據的情況,因此建議作為一個優化用戶體驗的方案,不能過多依賴。
參考文獻
后續計劃
W3C的規范文檔還在學習中。在有時間的情況下,后續可能會嘗試翻譯IndexedDB 2.0的官方文檔,如果有需要的后續可以繼續關注。有什么問題也歡迎隨時交流。