IndexedDB使用與出坑指南

黃Java發表於2019-03-04

概述

本文通過對IndexedDB的使用方法和使用場景進行相關介紹,對常見的問題進行解答。

同時,因為MDN中的相關文件缺乏相關邏輯性,所以不容易理解。本文將通過專案中常見的資料儲存和操作需求來進行內容組織。

讀者能夠通過本文學會在專案中正確的使用IndexedDB,給應用帶來的本地儲存能力,並且避免一些常見的問題。

原因:開發者需要在本地進行永久儲存

當我們進行一些較大的SPA頁面開發時,我們會需要進行一些資料的本地儲存。

當資料量不大時,我們可以通過SessionStorage或者LocalStorage來進行儲存,但是當資料量較大,或符合一定的規範時,我們可以使用資料庫來進行資料的儲存。

在瀏覽器提供的資料庫中,共有web sqlIndexedDB兩種。相較於HTML5已經廢棄的web sql來說,更推薦大家使用IndexedDB

結構

下面,我們通過一張圖來了解下,IndexedDB整體的結構。

IndexedDB使用與出坑指南

類比sql型資料庫,IndexedDB中的DB(資料庫)就是sql中的DB,而Object Store(儲存空間)則是資料表,Item則等於表中的一條記錄

使用IndexedDB

現在,我們將其根據IndexedDB的結構來對其操作進行介紹,能讓大家對這個儲存空間有一個初步的瞭解。我們主要介紹:

  • 資料庫操作
  • 資料表操作
  • 資料操作

資料庫操作

建立或開啟資料庫

使用IndexedDB第一步,就是建立或開啟一個資料庫。我們使用window.indexedDB.open(DBName)這個API來打進行操作。具體示例如下:

const request = window.indexedDB.open(`test`);

request.onupgradeneeded = function (event) {
    
}

request.onsuccess = function(event) {
	//request === event.target;
}
request.onerror = function(event) {}
複製程式碼

呼叫此介面時,如果當前資料庫不存在,則會建立一個新的資料庫。

當資料庫建立連線時,會返回一個IDBOpenDBRequest物件。

在連線建立成功時,會觸發onsuccess事件,其中函式引數eventtarget屬性就是request物件。

而在資料庫建立或者版本更新時,會觸發onupgradeneeded事件。

更新資料庫版本號

window.indexedDB.open的第二個引數即為版本號。在不指定的情況下,預設版本號為1。具體示例如下:

const request = window.indexedDB.open(`test`, 2);
複製程式碼

在需要更新資料庫的schema(模式)時,需要更新版本號。此時我們指定一個高於之前版本的版本號,就會觸發onupgradeneeded事件。類似的,當此資料庫不存在時,也會觸發此事件並且將版本更新到置頂版本。

我們需要注意的是,版本號是一個Unsigned long long數字,這意味著它可以是一個非常大的整數。但是,它不能是一個小數,否則它將會被轉為最近的整數,同時有可能導致onUpgradeneeded事件不觸發(bug)。

儲存空間操作

建立儲存空間

我們使用createObjectStore來建立一個儲存空間。同時,使用createIndex來建立它的索引。具體示例如下:

var request = window.indexedDB.open(`test`, 1);

request.onupgradeneeded = function (event) {
    var db = event.target.result;
    var objectStore = db.createObjectStore(`table1`, {keyPath: `id`, autoIncrement: true});

    objectStore.createIndex(`name`, `name`, {unique: false});
}

request.onerror = function (event) {
    alert("Why didn`t you allow my web app to use IndexedDB?!");
};
複製程式碼

注:只能在onupgradeneeded回撥函式中建立儲存空間,而不能在資料庫開啟後的success回撥函式中建立。

通過createObjectStore能夠建立一個儲存空間。接受兩個引數:

  1. 第一個引數,儲存空間的名稱,即我們上面的customers
  2. 第二個引數,指定儲存的keyPath值為儲存物件的某個屬性,這個屬效能夠在獲取儲存空間資料的時候當做key值使用。autoIncrement指定了key值是否自增(當key值為預設的從1開始到2^53的整數時)。

createIndex能夠給當前的儲存空間設定一個索引。它接受三個引數:

  1. 第一個引數,索引的名稱。
  2. 第二個引數,指定根據儲存資料的哪一個屬性來構建索引。
  3. 第三個屬性, options物件,其中屬性unique的值為true表示不允許索引值相等。

資料操作

事務

IndexedDB中,我們也能夠使用事務來進行資料庫的操作。事務有三個模式(常量已經棄用):

  • readOnly,只讀。
  • readwrite,讀寫。
  • versionchange,資料庫版本變化。

我們建立一個事務時,需要從上面選擇一種模式,如果不指定的話,則預設為只讀模式。具體示例如下:

const transaction = db.transaction([`customers`], `readwrite`);
複製程式碼

事務函式transaction的第一個引數為需要關聯的儲存空間,第二個可選引數為事務模式。與上面類似,事務成功時也會觸發onsuccess函式,失敗時觸發onerror函式。

事務的操作都是原子性的。

增加資料

當儲存空間初始化完成後,我們可以把資料放入儲存空間中。直接呼叫add方法就可以將資料放入儲存空間內,具體示例如下:

var request = window.indexedDB.open(`test`, 1);

request.onsuccess = function (event) {
    var db = event.target.result;

    var transaction = db.transaction([`table1`], `readwrite`);

    var objectStore = transaction.objectStore(`table1`);

    var index = objectStore.index(`name`);

    objectStore.add({name: `a`, age: 10});
    objectStore.add({name: `b`, age: 20});
}
複製程式碼

注:add方法中的第二個引數key值是指定儲存空間中的keyPath值,如果data中包含keyPath值或者此值為自增值,那麼可以略去此引數。

查詢資料

通過特定值獲取資料

當我們需要從儲存空間獲取資料時,我們可以通過以下的方法:

var request = window.indexedDB.open(`test`, 1);

request.onsuccess = function (event) {
    var db = event.target.result;

    var transaction = db.transaction([`table1`], `readwrite`);

    var objectStore = transaction.objectStore(`table1`);

    var request = objectStore.get(1);

    request.onsuccess = function (event) {
        // 對 request.result 做些操作!
        console.log(request.result);
    };

    request.onerror = function (event) {
        // 錯誤處理!
    };
}
複製程式碼

通過遊標獲取資料

當你需要遍歷整個儲存空間中的資料時,你就需要使用到遊標。遊標使用方法如下:

var request = window.indexedDB.open(`test`, 1);

request.onsuccess = function (event) {
    var db = event.target.result;

    var transaction = db.transaction([`table1`], `readwrite`);

    var objectStore = transaction.objectStore(`table1`);

    var request = objectStore.openCursor();

    request.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor) {
            // 使用Object.assign方法是為了避免控制檯列印時出錯
            console.log(Object.assign(cursor.value));
            cursor.continue();
        }
    };

    request.onerror = function (event) {
        // 錯誤處理!
    };
}
複製程式碼

使用遊標時有一個需要注意的地方,當遊標遍歷整個儲存空間但是並未找到給定條件的值時,仍然會觸發onsuccess函式。

openCursoropenKeyCursor有兩個引數:

  1. 第一個引數,遍歷範圍,指定遊標的訪問範圍。該範圍通過一個IDBKeyRange引數的方法來獲取。

    遍歷範圍引數具體示例如下:

// 匹配值 key === 1
const singleKeyRange = IDBKeyRange.only(1);

// 匹配值 key >= 1
const lowerBoundKeyRange = IDBKeyRange.lowerBound(1);

// 匹配值 key > 1
const lowerBoundOpenKeyRange = IDBKeyRange.lowerBound(1, true);

// 匹配值 key < 2
const upperBoundOpenKeyRange = IDBKeyRange.upperBound(2, true);

// 匹配值 key >= 1 && key < 2
const boundKeyRange = IDBKeyRange.bound(1, 2, false, true);

index.openCursor(boundKeyRange).onsuccess = function(event) {
  const cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};
複製程式碼

  1. 第二個引數,遍歷順序,指定遊標遍歷時的順序和處理相同id(keyPath屬性指定欄位)重複時的處理方法。改範圍通過特定的字串(IDBCursor的常量已經棄用)來獲取。其中:

    • next,從前往後獲取所有資料(包括重複資料)
    • prev,從後往前獲取所有資料(包括重複資料)
    • nextunique,從前往後獲取資料(重複資料只取第一條,索引重複即認為重複,下同)
    • prevunique,從後往前獲取資料(重複資料只取第一條)

    遍歷順序引數具體示例如下:

var request = window.indexedDB.open(`test`, 1);

request.onsuccess = function (event) {
    var db = event.target.result;

    var transaction = db.transaction([`table1`], `readwrite`);

    var objectStore = transaction.objectStore(`table1`);

    var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound(1, false);
    var request = objectStore.openCursor(lowerBoundOpenKeyRange, IDBCursor.PREV);

    request.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor) {
            // 使用Object.assign方法是為了避免控制檯列印時出錯
            console.log(Object.assign(cursor.value));
            cursor.continue();
        }
    };

    request.onerror = function (event) {
        // 錯誤處理!
    };
}
複製程式碼

使用索引

在前面構建資料庫時,我們建立了兩個索引。現在我們也可以通過索引來進行資料檢索。他的本質還是通過之前獲取資料的API來進行,只是將原來使用的keyPath屬性轉換成為了索引指定的屬性。具體示例如下:

var request = window.indexedDB.open(`test`, 1);

request.onsuccess = function (event) {
    var db = event.target.result;

    var transaction = db.transaction([`table1`], `readwrite`);

    var objectStore = transaction.objectStore(`table1`);

    var index = objectStore.index(`name`);

    // 第一種,get方法
    index.get(`a`).onsuccess = function (event) {
        console.log(event.target.result);
    }

    // 第二種,普通遊標方法
    index.openCursor().onsuccess = function (event) {
        console.log(`openCursor:`, event.target.result.value);
    }

    // 第三種,鍵遊標方法,該方法與第二種的差別為:普通遊標帶有value值表示獲取的資料,而鍵遊標沒有
    index.openKeyCursor().onsuccess = function (event) {
        console.log(`openKeyCursor:`, event.target.result);
    }
}
複製程式碼

修改資料

當需要修改儲存空間中的資料時,我們可以使用以下的API:

var objectStore = transaction.objectStore("customers");

var request = objectStore.put(data);

request.onsuccess = function (event) {
    
}
複製程式碼

注:put方法不僅能夠修改現有資料,也能夠往儲存空間中增加新的資料。

刪除資料

當我們需要刪除已經無用的資料時,我們可以通過以下方法:

var objectStore = transaction.objectStore("customers");

var request = objectStore.delete(name);

request.onsuccess = function (event) {
    
}
複製程式碼

異常處理

在瀏覽器有如下操作的情況下,indexedDB可能會出現異常:

  • 使用者清除瀏覽器快取
  • 儲存空間超過大小限制

此時,需要對錯誤進行捕獲,並且對使用者進行提示。此章節不是本文重點,再此略過。

擴充套件須知

取值相關

key值能夠接受的資料型別

IndexedDB中,鍵值對中的key值可以接受以下幾種型別的值:

  • number
  • data
  • string
  • binary
  • array

具體說明可以見文件此處

key path能夠接受的資料型別

當一個key值變為主鍵,即keyPath時,它的值就只能是以下幾種:

  • Blob
  • File
  • Array
  • String

注:空格不能出現在key path中

具體說明可以見文件此處

value能夠接受的資料型別

IndexedDB中,value能夠接受ECMA-262中所有的型別的值,例如String,Date,ImageDate等。

事務相關

事務中斷後,會不會影響key值的自增

IndexedDB在沒有指定key值的時候就會採用自增的key值。如果一個事務在中途中斷,那麼key值的自增將會從中斷的事務開始前的key開始。

安全相關

IndexedDB也受到瀏覽器同源策略的限制。

使用者相關

清空快取

使用者在清除瀏覽器快取時,可能會清除IndexedDB中相關的資料。

訪問許可權

部分瀏覽器如Safari手機版隱私模式在訪問IndexedDB時,可能會出現由於沒有許可權而導致的異常(LocalStorage也會),需要進行異常處理。

總結

IndexedDB在本地儲存中有著無可替代的作用,是替代關係型資料庫web sql的產品,能夠對大量資料進行儲存。在許多需要運用離線儲存的場景下,它能夠給我們提供有效的支撐。

但是,IndexedDB在使用過程中仍然需要避免可能會出現的一些問題,或者對可能導致的不利影響有一定的容錯處理。這樣才不會對應用產生重大影響。

參考文獻

相關文章