IndexedDB 打造靠譜 Web 離線數據庫

在知乎和我在日常工做中,經常會看到一個問題:前端

前端如今還火嗎?

這個我只想說:git

隔岸觀火的人永遠沒法明白起火的緣由,只有置身風暴,才能找到風眼之所在 ——『秦時明月』

你 TM 看都不看前端如今的發展,怎麼去評判前端火不火,我該不應嘗試一下其餘方面的內容呢?本人爲啥爲這麼熱衷於新的技術呢?主要緣由在於,生怕會被某一項顛覆性的內容淘汰掉,從前沿領域掉隊下來。說句人話就是:窮,因此只能學了...。因此本文會從頭剖析一下 IndexedDB 在前端裏面的應用的發展。github

indexedDB 目前在前端慢慢獲得普及和應用。它正朝着前端離線數據庫技術的步伐前進。之前一開始是 manifest、localStorage、cookie 再到 webSQL,如今 indexedDB 逐漸被各大瀏覽器承認。咱們也能夠針對它來進行技術上創新的開發。好比,如今小視頻很是流行,那麼咱們能夠在用戶觀看時,經過 cacheStorage 緩存,而後利用 WebRTC 技術實現 P2P 分發的控制,不過須要注意,必定要合理利用大小,否則後果然的很嚴重。web

indexedDB 的總體架構,是由一系列單獨的概念串聯而成,所有概念以下列表。一眼看去會發現沒有任何邏輯,不過,這裏我順手畫了一幅邏輯圖,中間會根據 函數 的調用而相互串聯起來。chrome

  • IDBRequest
  • IDBFactory
  • IDBDatabase
  • IDBObjectStore
  • IDBIndex
  • IDBKeyRange
  • IDBCursor
  • IDBTransaction

總體邏輯圖以下:數據庫

邏輯聯繫框圖

TL;DR

下文主要介紹了 indexedDB 的基本概念,以及在實際應用中的實操代碼。api

  • indexedDB 基礎概念。在 indexedDB 裏面會根據索引 index 來進行總體數據結構的劃分。
  • indexedDB 數據庫的更新是一個很是蛋疼的事情,由於,Web 的靈活性,你既須要作好向上版本的更新,也須要完善向下版本的容錯性。
  • indexedDB 高效索引機制,在內部,indexedDB 已經提供了 indexcursor等高效的索引機制,推薦不要直接將全部數據都取回來,再進行篩選,而是直接利用 cursor 進行。
  • 最後推薦幾個經常使用庫

離線存儲

IndexedDB 能夠存儲很是多的數據,好比 Object,files,blobs 等,裏面的存儲結構是根據 Database 來進行存儲的。每一個 DB 裏面能夠有不一樣的 object stores。具體結構以下圖:數組

indexedDB 結構圖

而且,咱們能夠給 key 設定相關特定的值,而後在索引的時候,能夠直接經過 key 獲得具體的內容。使用 IndexDB 須要注意,其遵循的是同域原則。promise

indexDB 基本概念

在 indexDB 中,有幾個基本的操做對象:瀏覽器

  • Database: 經過 open 方法直接打開,能夠獲得一個實例的 DB。每一個頁面能夠建立多個 DB,不過通常都是一個。
idb.open(name, version, upgradeCallback)
  • Object store: 這個就是 DB 裏面具體存儲的對象。這個能夠對應於 SQL 裏面的 table 內容。其存儲的結構爲:

image.png-392.5kB

  • index: 有點相似於外鏈,它自己是一種 Object store,主要是用來在本體的 store 中,索引另外 object store 裏面的數據。須要區別的是,key 和 index 是不同的。能夠參考: index DEMOmdn index。以下圖表示:

image.png-59.8kB

以下 code 爲:

// 建立 index
var myIndex = objectStore.index('lName');
  • transaction: 事務其實就是一系列 CRUD 的集合內容。若是其中一個環節失敗了,那麼整個事務的處理都會被取消。例如:
var trans1 = db.transaction("foo", "readwrite");
var trans2 = db.transaction("foo", "readwrite");
var objectStore2 = trans2.objectStore("foo")
var objectStore1 = trans1.objectStore("foo")
objectStore2.put("2", "key");
objectStore1.put("1", "key");
  • cursor: 主要是用來遍歷 DB 裏面的數據內容。主要是經過 openCursor 來進行控制。
function displayData() {
  var transaction = db.transaction(['rushAlbumList'], "readonly");
  var objectStore = transaction.objectStore('rushAlbumList');

  objectStore.openCursor().onsuccess = function(event) {
    var cursor = event.target.result;
    if(cursor) {
      var listItem = document.createElement('li');
      listItem.innerHTML = cursor.value.albumTitle + ', ' + cursor.value.year;
      list.appendChild(listItem);  

      cursor.continue();
    } else {
      console.log('Entries all displayed.');
    }
  };
}

如何使用 IndexDB

上面說了幾個基本的概念。那接下來咱們實踐一下 IndexDB。實際上入門 IndexDB 就是作幾個基本的內容

  • 打開數據庫表
  • 設置指定的 primary Key
  • 定義好索引的 index

前期搭建一個 IndexedDB 很簡單的代碼以下:

var request = indexedDB.open(dbName, 2);

request.onerror = function(event) {
  // 錯誤處理程序在這裏。
};
request.onupgradeneeded = function(event) {
  var db = event.target.result;
  // 設置 id 爲 primaryKey 參數
  var objectStore = db.createObjectStore("customers", { keyPath: "id",{autoIncrement:true} });
  
  // 設置指定索引,並確保惟一性
  objectStore.createIndex("name", "name", { unique: false });
  objectStore.createIndex("email", "email", { unique: true });

};

上面主要作了 3 件事:

  • 打開數據庫表
  • 新建 Store,並設置 primary Key
  • 設置 index

打開數據庫表主要就是版本號和名字,沒有太多講的,咱們直接從建立 store 開始吧。

建立 Object Store

使用的方法就是 IDBDatabase 上的 createObjectStore 方法。

var objectStore = db.createObjectStore("customers", { keyPath: "id",{autoIncrement:true} });

基本函數構造爲:

IDBObjectStore createObjectStore(DOMString name,
                                               optional IDBObjectStoreParameters options)
                                               
dictionary IDBObjectStoreParameters {
  (DOMString or sequence<DOMString>)? keyPath = null;
  boolean autoIncrement = false;
};
  • keyPath: 用來設置主鍵的 key,具體區別能夠參考下面的 keyPath 和 generator 的區別。
  • autoIncrement: 是否使用自增 key 的特性。

建立的 key 主要是爲了保證,在數據插入時惟一性的標識。

不過,每每一個主鍵(key),是沒辦法很好的完成索引,在具體實踐時,就還須要輔鍵 (aid-key) 來完成輔助索引工做,這個在 IndexDB 就映射爲 index

設置索引 index

在完成 PK(Primary key) 建立完畢後,爲了更好的搜索性能咱們還須要額外建立 index。這裏能夠直接使用:

objectStore.createIndex('indexName', 'property', options);
  • indexName: 設置當前 index 的名字
  • property: 從存儲數據中,指明 index 所指的屬性。

其中,options 有三個選項:

  • unique: 當前 key 是否能重複 (最經常使用)
  • multiEntry: 設置當前的 property 爲數組時,會給數組裏面每一個元素都設置一個 index 值。
# 建立一個名字叫 titleIndex 的 index,而且存儲的 index 不能重複
DB.createIndex('titleIndex', 'title', {unique: false});

具體能夠參考:MDN createIndex PropgoogleDeveloper Index

增刪數據

在 IndexedDB 裏面進行數據的增刪,都須要在 transaction 中完成。而這個增刪數據,你們能夠理解爲一次 request,至關於在一個 transaction 裏面管理全部當前邏輯操做的 request。因此,在正式開始進行數據操做以前,還須要給你們簡單介紹一些若是建立一個事務。

事務的建立

transaction API,以下 [代碼1]。在建立時,你須要手動指定當前 transaction 是那種類型的操做,基本的內容有:

  • "readonly":只讀
  • "readwrite":讀寫
  • "versionchange":這個不能手動指定,會在 upgradeneeded 回調事件裏面自動建立。它能夠用來修改現有 object store 的結構數據,好比 index 等。

你能夠經過在數據庫打開以後,經過 IDBDataBase 上的 transaction 方法建立,如 [代碼2]。

[代碼1]
  [NewObject] IDBTransaction transaction((DOMString or sequence<DOMString>) storeNames,
                                         optional IDBTransactionMode mode = "readonly");
                                         
[代碼2]
var transaction = db.transaction(["customers"], "readwrite");
var objectStore = transaction.objectStore("customers");
# 遍歷存儲數據
for (var i in customerData) {
  var request = objectStore.add(customerData[i]);
  request.onsuccess = function(event) {
    // success, done?
  };
}

事務在建立的時候不只僅能夠制定執行的模式,還能夠指定本次事務可以影響的 ObjectStore 範圍,具體細節就是在第一個 transaction 參數裏面傳入的是一個數據,而後經過 objectStore() 方法打開多個 OS 進行操做,以下 [代碼3]。

[代碼3]
var tx = db.transaction(["books","person"], "readonly");
var books = tx.objectStore("books");
var person = tx.objectStore("person");

操做數據

完成了事務的建立以後,咱們就能夠正式的開始進行數據的交互操做了,也就是寫咱們具體的業務邏輯。以下 [代碼1],一個完整數據事務的操做。

[代碼1]
var tx = db.transaction("books", "readwrite");
var store = tx.objectStore("books");

store.put({title: "Quarry Memories", author: "Fred", isbn: 123456});
store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567});
store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678});

tx.oncomplete = function() {
  // All requests have succeeded and the transaction has committed.
};

經過 objectStore 回調獲得的 IDBObjectStore 對象,咱們就能夠進行一些列的增刪查改操做了。能夠參考 [代碼2]。詳細的能夠參考文末的 appendix

[代碼2]
  [NewObject] IDBRequest put(any value, optional any key);
  [NewObject] IDBRequest add(any value, optional any key);
  [NewObject] IDBRequest delete(any query);

索引數據

索引數據是全部數據庫裏面最重要的一個。這裏,咱們可使用遊標,index 來作。例如,經過 index 來快速索引 key 值,參考 [代碼1]。

[代碼1]
var index = objectStore.index("name");
index.get("Donna").onsuccess = function(event) {
  alert("Donna's SSN is " + event.target.result.ssn);
};

更詳細的內容,能夠參考下文 數據索引方式

keyPath 和 key Generator

何謂 keyPath 和 keyGenerator 應該算是 IndexedDB 裏面比較難以理解的概念。簡單來講,IndexedDB 在建立 Store 的時候,必須保證裏面的數據是惟一的,那麼得須要像其它數據庫同樣設置一個 primary Key 來區分不一樣數據。而 keyPath 和 Generator 就是兩種不一樣的設置 key 的方式。

設置 keyPath

# 設置預先須要存放的數據

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" }
];

# 經過 keyPath 設置 Primary Key
var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

由於 ssn 在該數據集是惟一的,因此,咱們能夠利用它來做爲 keyPath 保證 unique 的特性。或者,能夠設置爲自增的鍵值,好比 id++ 相似的。

upgradeDb.createObjectStore('logs', {keyPath: 'id', autoIncrement:true});

使用 generator

generator 會每次在添加數據時,自動建立一個 unique value。這個 unique value 是和你的實際數據是分開的。裏面直接經過 autoIncrement:true 來設置便可。

upgradeDb.createObjectStore('notes', {autoIncrement:true});

indexDB 打開注意事項

檢查是否支持 indexDB

if (!('indexedDB' in window)) {
  console.log('This browser doesn\'t support IndexedDB');
  return;
}

版本更新: indexDB

在生成一個 indexDB 實例時,須要手動指定一個版本號。而最經常使用的

idb.open('test-db7', 2, function(upgradeDb) {})

這樣會形成一個問題,好比上線過程當中,用戶A第一次請求返回了新版本的網頁,鏈接了版本2。以後又刷新網頁命中了另外一臺未上線的機器,鏈接了舊版本1 出錯。主要緣由是:

indexedDB API 中不容許數據庫中的數據倉庫在同一版本中發生變化. 而且當前 DB 版本不能和低版本的 version 鏈接。

好比,你一開始定義的 DB 版本內容爲:

# 版本必定義的內容
db.version(1).stores({friends: "++id,name"});

# 版本二修改結構爲:
db.version(2).stores({friends: "++id,name,shoeSize"});

若是此時,用戶先打開了 version(1),可是後面,又獲得的是 version(2) 版本的 HTML,這時就會出現 error 的錯誤。

參考:

版本更替

版本更新

這個在 IndexDB 是一個很重要的問題。主要緣由在於

indexedDB API 中不容許數據庫中的數據倉庫在同一版本中發生變化. 而且當前 DB 版本不能和低版本的 version 鏈接。

上面就能夠抽象爲一個問題:

你什麼狀況下須要更新 IndexDB 的版本呢?
  1. 該表數據庫裏面的 keyPath 時。
  2. 你須要從新設計數據庫表結構時,好比新增 index
# 版本 1 的 DB 設計,有一個主鍵 id 和 index-name
db
.version(1)
.stores({friends: '++id,name'})

# 若是直接想新增一個 key,例如 male,是沒法成功的
db
.version(1)
.stores({friends: '++id,name,male'})

# 正確辦法是直接修改版本號更新
db
.version(2)
.stores({friends: '++id,name,male'})

不過,若是直接修改版本號,會出現這樣一個 case:

  • 因爲原始 HTML 更新問題,用戶首先訪問的是版本 1 的 A 頁面,而後,訪問更新事後的 B 頁面。這時,IndexDB 成功更新爲高版本。可是,用戶下次又命中了老版本的 A 頁面,此時 A 中仍是鏈接低版本的 IndexDB ,就會報錯,致使你訪問失敗。

解決辦法就是,設置過濾,在 open 的時候,手動傳入版本號:

# 打開版本 1 的數據庫
var dbPromise = idb.open('db1', 1, function(upgradeDb){...})

# 打開版本 2 的數據庫
var dbPromise = idb.open('db2', 2, function(upgradeDb){...})

不過,這樣又會形成另一個問題,即,數據遷移(老版本數據,不可能不要吧)。這裏,IndexDB 會有一個 updateCallback 給你觸發,你能夠直接在裏面作相關的數據遷移處理。

var dbPromise = idb.open('test-db7', 2, function(upgradeDb) {
  switch (upgradeDb.oldVersion) {
    case 0:
      upgradeDb.createObjectStore('store', {keyPath: 'name'});
    case 1:
      var peopleStore = upgradeDb.transaction.objectStore('store');
      peopleStore.createIndex('price', 'price');
  }
});

在使用的時候,必定要注意 DB 版本的升級處理,好比有這樣一個 case,你的版本已是 3,不過,你須要處理版本二的數據:

# 將版本二 中的 name 拆分爲  firstName 和 lastName
db.version(3).stores({friends: "++id,shoeSize,firstName,lastName"}).upgrade(function(t) {
    
    return t.friends.toCollection().modify(function(friend) {
        // Modify each friend:
        friend.firstName = friend.name.split(' ')[0];
        friend.lastName = friend.name.split(' ')[1];
        delete friend.name;
    });
});

對於存在版本 2 數據庫的用戶來講是 OK 的,可是對於某些尚未訪問過你數據庫的用戶來講,這無疑就報錯了。解決辦法有:

  • 保留每一個版本時,建立的字段和 stores
  • 在更新 callback 裏面,對處理的數據判斷是否存在便可。

在 Dexie.js DB 數據庫中,須要你保留每次 DB 建立的方法,其實是經過 添加 swtich case ,來完成每一個版本的更新:

# Dexie.js 保留 DB 數據庫
db.version(1).stores({friends: "++id,name"});
db.version(2).stores({friends: "++id,name,shoeSize"});
db.version(3).stores({friends: "++id,shoeSize,firstName,lastName"}).upgrade(...)

# 內部原理,直接添加 switch case 完成版本更新
var dbPromise = idb.open('test-db7', 2, function(upgradeDb) {
  switch (upgradeDb.oldVersion) {
    case 0:
      upgradeDb.createObjectStore('store', {keyPath: 'name'});
    case 1:
      var peopleStore = upgradeDb.transaction.objectStore('store');
      peopleStore.createIndex('price', 'price');
  }
});

若是遇到一個頁面打開,可是另一個頁面拉取到新的代碼進行更新時,這個時候還須要將低版本 indexedDB 進行顯式的關閉。具體操做辦法就是監聽 onversionchange 事件,當版本升級時,通知當前 DB 進行關閉,而後在新的頁面進行更新操做。

openReq.onupgradeneeded = function(event) {
  // 全部其它數據庫都已經被關掉了,直接更新代碼
  db.createObjectStore(/* ... */);
  db.onversionchange = function(event) {
    db.close();
  };

}

最後,更新是還有幾個注意事項:

  • 版本更新不能改變 primary key
  • 回退代碼時,千萬注意版本是否已經更新。不然,只能增量更新,從新修改版本號來修復。

存儲加密特性

有時候,咱們存儲時,想獲得一個由一串 String 生成的 hash key,那在 Web 上應該如何實現呢?

這裏能夠直接利用 Web 上已經實現的 WebCrypto,爲了實現上述需求,咱們能夠直接利用裏面的 digest 方法便可。這裏 MDN 上,已經有現成的辦法,咱們直接使用便可。

參考:

WebCrypto 加密手段

存儲上限值

基本限制爲:

瀏覽器 限制
Chrome 可用空間 < 6%
Firebox 可用空間 < 10%
Safari < 50MB
IE10 < 250MB

逐出策略爲:

瀏覽器 逐出政策
Chrome 在 Chrome 耗盡空間後採用 LRU 策略
Firebox 在整個磁盤已裝滿時採用 LRU 策略
Safari 無逐出
Edge 無逐出

參考:

存儲上限值
瀏覽器內核存儲上限值處理

數據索引方式

在數據庫中除了基本的 CRUD 外,一個高效的索引架構,則是裏面的重中之重。在 indexedDB 中,咱們一共能夠經過三種方式來索引數據:

  • 固定的 key 值
  • 索引外鍵(index)
  • 遊標(cursor)

固定 key 索引

IDBObjectStore 提供給了咱們直接經過 primaryKey 來索引數據,參考 [代碼1],這種方式須要咱們一開始就知道目標的 key 內容。固然,也能夠經過 getAll 所有索引數據。

[代碼1]
  [NewObject] IDBRequest get(any query);
  [NewObject] IDBRequest getKey(any query);
  [NewObject] IDBRequest getAll(optional any query,
                                optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest getAllKeys(optional any query,
                                    optional [EnforceRange] unsigned long count);

好比,咱們經過 primaryKey 獲得一條具體的數據:

db.transaction("customers").objectStore("customers").get("id_card_1118899").onsuccess = function(event) {
    // data is event.target.result.name
};

也能夠 fetch 整個 Object Store 的數據。這些場景用處比較少,這裏就不過多講解。咱們主要來了解一下 index 的索引方式。

index 索引

若是想要查詢某個數據,直接經過整個對象來進行遍歷的話,這樣作性能耗時是很是大的。若是咱們結合 index 來將 key 加以分類,就能夠很快速的實現指定數據的索引。這裏,咱們能夠直接利用 IDBObjectStore 上面的 index() 方法來獲取指定 index 的值,具體方法能夠參考 [代碼1]。

[代碼1]
 IDBIndex index(DOMString name);

該方法會直接返回一個 IDBIndex 對象。這你也能夠理解爲一個相似 ObjectStore 的微型 index 數據內容。接着,咱們可使用 get() 方法來得到指定 index 的數據,參考[代碼2]。

[代碼2]
var index = objectStore.index("name");
index.get("Donna").onsuccess = function(event) {
  alert("Donna's SSN is " + event.target.result.ssn);
};

使用 get 方法無論你的 index 是不是 unique 的都會只會返回第一個數據。若是想獲得多個數據的話,可使用 getAll(key) 來作。經過 getAll() 獲得的回調函數,直接經過 event.target.result 能夠獲得對應的 value 內容。

objectStore.getAll().onsuccess = function(event) {
      printf(event.target.result); // Array
    };

除了經過 getAll() 獲得全部數據外,還能夠採用更高效的 cursor 方法遍歷獲得的數據。

參考:

getAll() 和 openCursor 實例

遊標索引

所謂的遊標,你們內心應該能夠有一個初步的印象,就像咱們物理尺子上的那個東西,能夠自由的移動,來標識指向的對象內容。cursor 裏面有兩個核心的方法:

  • advance(count): 將當前遊標位置向前移動 count 位置
  • continue(key): 將當前遊標位置移動到指定 key 的位置,若是沒提供 key 則表明的移動下一個位置。

好比,咱們使用 cursor 來遍歷 Object Store 的具體數據。

objectStore.openCursor().onsuccess = function(event) {
    var cursor = event.target.result;
    if(cursor) {
        // cursor.key 
        // cursor.value
      cursor.continue();
    } else {
      console.log('Entries all displayed.');
    }
  };

一般,遊標能夠用來遍歷兩個類型的數據,一個是 ObjectStore、一個是 Index。

  • Object.store: 若是在該對象上使用遊標,那麼會根據 primaryKey 遍歷整個數據,注意,這裏不會存在重複的狀況,由於 primaryKey 是惟一的。
  • index: 在 index 上使用遊標的話,會以當前的 index 來進行遍歷,其中可能會存在重複的現象。

在 IDBObjectStore 對象上有兩種方法來打開遊標:

  • openCursor: 遍歷的對象是 具體的數據值,最經常使用的方法
  • openKeyCursor: 遍歷的對象是 數據 key 值

這裏,咱們經過 openCursor 來直接打開一個 index 數據集,而後進行遍歷。

PersonIndex.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  }
  else {
    alert("Got all customers: " + customers);
  }
};

在遊標中,還提供給了一個 updatedelete 方法,咱們能夠用它來進行數據的更新操做,不然的話就直接使用 ObjectStore 提供的 put 方法。

遊標裏面咱們還能夠限定其遍歷的範圍和方向。這個設置是咱們直接在 openCursor() 方法裏面傳參完成的,該方法的構造函數參考 [代碼1]。他裏面能夠傳入兩個參數,第一個用來指定範圍,第二個用來指定 cursor 移動的方向。

[代碼1]
IDBRequest openCursor(optional any query,
                                    optional IDBCursorDirection direction = "next");

若是須要對 cursor 設置範圍的話,就須要使用到 IDBKeyRange 這個對象,使用樣板能夠參考 [代碼2]。IDBKeyRange 裏面 key 參考的對象 因使用者的不一樣而不一樣。若是是針對 ObjectStore 的話,則是針對 primaryKey,若是是針對 Index 的話,則是針對當前的 indexKey

/ 匹配全部在 「Bill」 前面的, 可是不須要包括 "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

好比,咱們這裏對 PersonIndex 設置一個 index 範圍,即,索引 在 villainhrjimmyVV 之間的數據集合。

# 都包括 villainhr 和 jimmyVV 的數據
var boundKeyRange = IDBKeyRange.bound("villainhr", "jimmyVV", true, true);

 PersonIndex.openCursor(boundKeyRange).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

若是你還想設置遍歷的方向和是否排除重複數據,還能夠根據 [代碼2] 的枚舉類型來設置。好比,在 [代碼3] 中,咱們改變默認的 cursor 遍歷數據的方向爲 prev,從末尾開始。

[代碼2]
enum IDBCursorDirection {
  "next",
  "nextunique",
  "prev",
  "prevunique"
};

[代碼3]
objectStore.openCursor(null, IDBCursor.prev).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.value 
    cursor.continue();
  }
};

事務讀取性能

在 indexDB 裏面的讀寫所有是基於 transaction 模式來的。也就是 IDBDataBase 裏面的 transaction 方法,以下 [代碼1]。全部的讀寫均可以比做在 transaction 做用域下的請求,只有當全部請求完成以後,該次 transaction 纔會生效,不然就會拋出異常或者錯誤。transaction 會根據監聽 error,abort,以及 complete 三個事件來完成整個事務的流程管理,參考[代碼2]。

[代碼1]
  [NewObject] IDBTransaction transaction((DOMString or sequence<DOMString>) storeNames,
                                         optional IDBTransactionMode mode = "readonly");

[代碼2]
  attribute EventHandler onabort;
  attribute EventHandler oncomplete;
  attribute EventHandler onerror;

例如:

var request = db.transaction(["customers"], "readwrite")
                .objectStore("customers")
                .delete("gg");
request.onsuccess = function(event) {
  // delete, done
};

你能夠在 transaction 方法裏面手動傳入 readwrite 或者其餘表示事務的 readonly 參數,來表示本次事務你會進行如何的操做。IndexedDB 在初始設計時,就已經決定了它的性能問題。

只含有 readonly 模式的 transaction 能夠併發進行執行
含有 write 模式的 transaction 必須按照隊列 來 執行

這就意味着,若是你使用了 readwrite 模式的話,那麼後續無論是否是 readonly 都必須等待該次 transaction 完成才行。

經常使用技巧

生成 id++ 的主鍵

指定 primaryKey 生成時,是經過 createObjectStore 方法來操做的。有時候,咱們會遇到想直接獲得一個 key,而且存在於當前數據集中,能夠在 options 中同時加上 keyPathautoIncrement 屬性。該 key 的範圍是 [1- $ 2^{53} $],參考 keygenerator key 的大小

db.createObjectStore('table1', {keyPath: 'id', autoIncrement: true});

推薦

閱讀推薦

indexedDB W3C 文檔
indexedDB 入門
MDN indexedDB 入門

好用庫推薦

idb: 一個 promise 的 DB 庫

Indexed Appendix

  • IndexedDB 數據庫使用key-value鍵值對儲存數據.你能夠對對象的某個屬性建立索引(index)以實現快速查詢和列舉排序。.key可使二進制對象
  • IndexedDB 是事務模式的數據庫. IndexedDB API提供了索引(indexes), 表(tables), 指針(cursors)等等, 可是全部這些必須是依賴於某種事務的。
  • The IndexedDB API 基本上是異步的.
  • IndexedDB 數據庫的請求都會包含 onsuccess和onerror事件屬性。
  • IndexedDB 在結果準備好以後經過DOM事件通知用戶
  • IndexedDB是面向對象的。indexedDB不是用二維表來表示集合的關係型數據庫。這一點很是重要,將影響你設計和創建你的應用程序。
  • indexedDB不使用結構化查詢語言(SQL)。它經過索引(index)所產生的指針(cursor)來完成查詢操做,從而使你能夠迭代遍歷到結果集合。
  • IndexedDB遵循同源(same-origin)策略

侷限和移除 case

  • 全球多種語言混合存儲。國際化支持很差。須要本身處理。
  • 和服務器端數據庫同步。你得本身寫同步代碼。
  • 全文搜索。

在如下狀況下,數據庫可能被清除:

  • 用戶請求清除數據。
  • 瀏覽器處於隱私模式。最後退出瀏覽器的時候,數據會被清除。
  • 硬盤等存儲設備的容量到限。
  • 不正確的
  • 不完整的改變.

常規概念

數據庫

  • 數據庫: 一般包含一個或多個 object stores. 每一個數據庫必須包含如下內容:

    • 名字(Name): 它標識了一個特定源中的數據庫,而且在數據庫的整個生命週期內保持不變. 此名字能夠爲任意字符串值(包括空字符串).
    • 當前版本(version). 當一個數據庫首次建立時,它的 version 爲1,除非另外指定. 每一個數據庫在任意時刻只能有一個 version
  • 對象存儲(object store): 用來承載數據的一個分區.數據以鍵值對形式被對象存儲永久持有。在 OS 中,建立一個 key 可使用 key generatorkey path

    • key generator: 簡單來講就是在存儲數據時,主動生成一個 id++ 來區分每條記錄。這種狀況下 存儲數據的 key 是和 value 分開進行存儲的,也就是 (out of line)。
    • key path: 須要用戶主動來設置儲存數據的 key 內容,
    • request: 每次讀寫操做,能夠當作一次 request.
    • transaction: 一系列讀寫請求的集合。
    • index: 一個特殊的 Object Store,用來索引另一個 Store 的數據。
  • 具體數據 key/value

    • key: 這個 key 的值,能夠經過三種方式生成。 a key generator, a key path, 用戶指定的值。而且,這個 key 在當前的 Object Store 是惟一的。一個 key 類型能夠是 string, date, float, and array 類型。不過,在老版本的時候,通常只支持 string or integer。(如今,版本應該都 OK 了)

      - key generator: 至關於以一種 `id++` 的形式來生成一個 key 值。
      - key path: 當前指定的 key 能夠根據 value 裏面的內容來指定。裏面能夠爲一些分隔符。
      - 指定的 key:這個就是須要用戶手動來指定生成。
      • value: 能夠存儲 boolean, number, string, date, object, array, regexp, undefined, and null。如今還能夠存儲 files and blob 對象。

操做做用域

  • scope:這能夠比做 transaction 的做用域,即,一系列 transaction 執行的順序。該規定,多個 reading transaction 可以同時執行。可是 writing 則只能排隊進行。
  • key range: 用來設置取出數據的 key 的範圍內容。

參考:

原生概念 IndexedDB

IDBFactory

這其實就是 indexDB 上面掛載的對象。主要 API 以下:

[Exposed=(Window,Worker)]
interface IDBFactory {
  [NewObject] IDBOpenDBRequest open(DOMString name,
                                    optional [EnforceRange] unsigned long long version);
  [NewObject] IDBOpenDBRequest deleteDatabase(DOMString name);

  short cmp(any first, any second);
};

你能夠直接經過 open 來打開一個數據庫。經過 返回一個 Request 對象,來進行結果監聽的回調:

var request = indexedDB.open('AddressBook', 15);
request.onsuccess = function(evt) {...};
request.onerror = function(evt) {...};

參考:

IndexDB Factory API

IDBRequest

當你經過 open 方法處理事後,就會獲得一個 Request 回調對象。這個就是 IDBRequest 的實例。

[Exposed=(Window,Worker)]
interface IDBRequest : EventTarget {
  readonly attribute any result; // 經過 open 打開事後的 IDBObjectStore 實例 
  readonly attribute DOMException? error;
  readonly attribute (IDBObjectStore or IDBIndex or IDBCursor)? source;
  readonly attribute IDBTransaction? transaction;
  readonly attribute IDBRequestReadyState readyState;

  // Event handlers:
  attribute EventHandler onsuccess;
  attribute EventHandler onerror;
};

enum IDBRequestReadyState {
  "pending",
  "done"
};

[Exposed=(Window,Worker)]
interface IDBOpenDBRequest : IDBRequest {
  // Event handlers:
  attribute EventHandler onblocked;
  attribute EventHandler onupgradeneeded;
};

你能夠經過 result 獲得當前數據庫操做的結果。若是你打開更新後的版本號的話,還須要監聽 onupgradeneeded 事件來實現。最常經過 indexedDB.open 碰見的錯誤就是 VER_ERR 版本錯誤。這代表存儲在磁盤上的數據庫的版本高於你試圖打開的版本。

db.onerror = function(event) {
  // Generic error handler for all errors targeted at this database's
  // requests!
  alert("Database error: " + event.target.errorCode);
};

因此,通常在建立 IndexDB 時,還須要管理它版本的更新操做,這裏就須要監聽 onupgradeneeded 來是實現。

request.onupgradeneeded = function(event) { 
   // 更新對象存儲空間和索引 .... 
};

或者咱們能夠直接使用 idb 微型庫來實現讀取操做。

var dbPromise = idb.open('test-db3', 1, function(upgradeDb) {
    if (!upgradeDb.objectStoreNames.contains('people')) {
      upgradeDb.createObjectStore('people', {keyPath: 'email'});
    }
    if (!upgradeDb.objectStoreNames.contains('notes')) {
      upgradeDb.createObjectStore('notes', {autoIncrement: true});
    }
    if (!upgradeDb.objectStoreNames.contains('logs')) {
      upgradeDb.createObjectStore('logs', {keyPath: 'id', autoIncrement: true});
    }
  });

其中經過 onupgradeneeded 回調獲得的 event.result 就是 IDBDatabase 的實例,經常用來設置 index 和插入數據。參考下面內容。

參考:

IDBRequest API

IDBDatabase

該對象經常用來作 Object Store 和 transaction 的建立和刪除。該部分是 onupgradeneeded 事件得到的 event.target.result 對象:

request.onupgradeneeded = function(event) { 
   // 更新對象存儲空間和索引 .... 
   // event.target.result 對象
};

具體 API 內容以下:

[Exposed=(Window,Worker)]
interface IDBDatabase : EventTarget {
  readonly attribute DOMString name;
  readonly attribute unsigned long long version;
  readonly attribute DOMStringList objectStoreNames;

  [NewObject] IDBTransaction transaction((DOMString or sequence<DOMString>) storeNames,
                                         optional IDBTransactionMode mode = "readonly");
  void close();

  [NewObject] IDBObjectStore createObjectStore(DOMString name,
                                               optional IDBObjectStoreParameters options);
  void deleteObjectStore(DOMString name);

  // Event handlers:
  attribute EventHandler onabort;
  attribute EventHandler onclose;
  attribute EventHandler onerror;
  attribute EventHandler onversionchange;
};

dictionary IDBObjectStoreParameters {
  (DOMString or sequence<DOMString>)? keyPath = null;
  boolean autoIncrement = false;
};

若是它經過 createObjectStore 方法,那麼獲得的就是一個 IDBObjectStore 實例對象。若是是 transaction 方法,那麼就是 IDBTransaction 對象。

IDBObjectStore

該對象通常是用來建立 index 和插入數據使用。

能夠參考:

[Exposed=(Window,Worker)]
interface IDBObjectStore {
  attribute DOMString name;
  readonly attribute any keyPath;
  readonly attribute DOMStringList indexNames;
  [SameObject] readonly attribute IDBTransaction transaction;
  readonly attribute boolean autoIncrement;

  [NewObject] IDBRequest put(any value, optional any key);
  [NewObject] IDBRequest add(any value, optional any key);
  [NewObject] IDBRequest delete(any query);
  [NewObject] IDBRequest clear();
  [NewObject] IDBRequest get(any query);
  [NewObject] IDBRequest getKey(any query);
  [NewObject] IDBRequest getAll(optional any query,
                                optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest getAllKeys(optional any query,
                                    optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest count(optional any query);

  [NewObject] IDBRequest openCursor(optional any query,
                                    optional IDBCursorDirection direction = "next");
  [NewObject] IDBRequest openKeyCursor(optional any query,
                                       optional IDBCursorDirection direction = "next");

  IDBIndex index(DOMString name);

  [NewObject] IDBIndex createIndex(DOMString name,
                                   (DOMString or sequence<DOMString>) keyPath,
                                   optional IDBIndexParameters options);
  void deleteIndex(DOMString name);
};

dictionary IDBIndexParameters {
  boolean unique = false;
  boolean multiEntry = false;
};

IDBIndex

該對象是用來進行 Index 索引的操做對象,裏面也會存在 getgetAll 等方法。詳細內容以下:

[Exposed=(Window,Worker)]
interface IDBIndex {
  attribute DOMString name;
  [SameObject] readonly attribute IDBObjectStore objectStore;
  readonly attribute any keyPath;
  readonly attribute boolean multiEntry;
  readonly attribute boolean unique;

  [NewObject] IDBRequest get(any query);
  [NewObject] IDBRequest getKey(any query);
  [NewObject] IDBRequest getAll(optional any query,
                                optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest getAllKeys(optional any query,
                                    optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest count(optional any query);

  [NewObject] IDBRequest openCursor(optional any query,
                                    optional IDBCursorDirection direction = "next");
  [NewObject] IDBRequest openKeyCursor(optional any query,
                                       optional IDBCursorDirection direction = "next");
};

參考:

idb 開源庫,微型代碼庫
treo 開源庫
dexie.js 開源庫
indexeddb
原生概念 IndexedDB

也歡迎你們關注個人公衆號:前端小吉米 得到一手的技術文章以及將來技術的發展內容。

圖片描述

相關文章
相關標籤/搜索