簡介 IndexedDB 與 詳解 IndexedDB 在實際項目中可能遇到的問題與解決方案

簡介IndexedDB
詳細文檔請參看 MDN 文檔連接
IndexedDB能作什麼:
  1. 它真的很能存東西!對比cookie,local storeage,session storage 等受到大小限制的web存儲方式,IndexedDB在理論上並沒有大小限制只與本地的磁盤相關。這也是選擇它做爲web本地存儲工具最大的理由。
  2. 完整的API文檔(雖然大部分是英文Orz),不懂的直接翻文檔。
  3. 異步,這意味着不會鎖死瀏覽器,也意味着在進行多庫多表操做時會很麻煩,下文中會詳細介紹。

廢話很少說,讓咱們直接用一個實際的例子來看 IndexedDB 如何使用。(PS:這一部分算是對 IndexedDB 的簡介和科普,本文真正的核心在後面,若是不想看科普能夠直接跳到後面)html

IndexedDB 須要理解如下幾個重要的概念:
數據庫:  IDBDatabase 
對象倉庫:IDBObjectStore (我更願意稱之爲:表)
索引: IDBIndex 
事務: IDBTransaction 
操做請求:IDBRequest 
指針: IDBCursor

實際項目中通常正常的流程爲:web

開庫 → 建表 → 建立索引 → 存入/刪除數據 → 獲取數據數據庫

這裏咱們先使用文檔中的一個例子(後面再來講哪裏存在問題)數組

const dbName = "the_name";

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" }];
  
var request = indexedDB.open(dbName, 2);

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

request.onupgradeneeded = function(event) {
  var db = event.target.result;
  
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  objectStore.createIndex("name", "name", { unique: false });
  objectStore.createIndex("email", "email", { unique: true });
  
  objectStore.transaction.oncomplete = function(event) {
    var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers");
    customerData.forEach(function(customer) {
      customerObjectStore.add(customer);
    });
  };
};

開庫:

indexedDB.open(庫名,數據庫版本)
注意事項:
  1. 庫名必填,版本非必填
  2. 版本號若是不填則,默認打開當前版本的數據庫
  3. 這個函數的回調即上文中提到的重要概念之一 IDBRequest
IDBRequest:
一般來講咱們常常會用到的函數
onsuccess : 成功回調,通俗的講就是:你能夠開始頁面的其餘操做了。
onupgradeneeded :升級數據庫回調,通俗的講就是:穩一手,再操做。
注意事項
  1. onupgradeneeded 優先於 onsuccess 觸發
  2. 當僅當數據庫版本號 發生變化的時候觸發 onupgradeneeded 。換句話說,若是當前版本號爲2。promise

    1. indexedDB.open('myDB') 只會觸發 onsuccess 。
    2. indexedDB.open('myDB', 3) 同時觸發 onsuccess 與 onupgradeneeded 。優先級參看第1條。
    3. indexedDB.open('myDB', 1) 什麼都不會發生 :)
  3. 當僅當觸發 onupgradeneeded 時 能夠對 IDBObjectStore 也就是表進行增、刪、改。

建表:

event.target.result.createObjectStore('myList',{ keyPath: 'id', autoIncrement: true })
注意事項:
  1. 第一個參數表名,第二個參數 keyPath 主鍵名,autoIncrement 主鍵是否自增。
  2. 這裏有個很隱晦的坑,若是設置主鍵自增,那麼在建立索引的時候能夠無需傳入主鍵名,反之則須要傳入主鍵名,後續的例子中會呈現。
  3. event.target.result 是函數 onupgradeneeded 的返回值,同時也是上文提到的重要概念之一 IDBDatabase 以及它的方法 IDBTransaction
IDBDatabase
這個對象就是一般意義上的數據庫自己,咱們能夠經過這個對象進行表的增、刪,以及事物 IDBTransaction 。
IDBTransaction
在IndexedDB中所作的全部事情老是發生在事務的上下文中,表示與數據庫中的數據的交互。
IndexedDB中的全部對象——包括對象存儲、索引和遊標等都與特定事務綁定。
所以,在事務以外不能執行命令、訪問數據或打開任何東西。
(PS: 通俗的意義上講就是...此路是我開,此樹是我栽,要想讀寫數據,請過我這關  ̄□ ̄ )

建立索引

objectStore.createIndex("name", "name", { unique: false });
注意事項
  1. 第一個和第二個參數均是索引名,unique 若是爲true,則索引將不容許單個鍵有重複的值。
  2. objectStore 即 IDBObjectStore 也就是表。
  3. 表數據的增、刪、改能夠放在 onupgradeneeded 或 onsuccess 中進行(推薦在 onsuccess 中),可是對於表自己和索引的修改僅能在 onupgradeneeded 中。
IDBObjectStore
這個就是表了,它所包含的方法不少都是實際項目中常常用到的好比:
add() 寫入數據
createIndex() 建立索引
delete() 刪除鍵
index() 獲取索引
get() 檢索值
getAll() 檢索全部的值
不作過多敘述,詳見文檔。
注意事項
  1. 再次重複一遍,這個對象包含的方法涵蓋了對錶自己以及數據的操做。對自己的操做請在 onupgradeneeded 中,對數據的操做請在 onsuccess 中。

存入/刪除數據

還記得 IDBTransaction 和 IDBObjectStore 嗎?此時繞不開這倆貨
雖然說真正執行數據操做的函數是 objectStore.add() 等等,但請在事物IDBTransaction中獲取IDBObjectStore對象。

獲取數據

同上,原諒我,懶得寫 :) 了

正片開始

若是光看上面的例子,其實 IndexedDB 並不複雜。然而在實際項目中卻會遇到大量的問題,主要集中在1個問題所引起更多的小問題。瀏覽器

這個問題就是:多庫或多表的同時操做。 這也是本文真正的想要表達的東西cookie

在實際項目中,不太可能一張表就寫完全部數據,有過數據庫操做經驗的老哥應該明白。一般咱們須要關聯兩張甚至多張表,即一張表的鍵值,是另外一張表的鍵或主鍵,因此咱們能夠關聯這兩張表,而沒必要要也不須要在一張表裏寫完全部數據。session

因爲 IndexedDB 是異步實現,因此首先要明確咱們究竟在操做哪張表,創建了哪一個事物,這個連接完成了嗎?等等。異步

明確上述問題才能解決:爲什麼索引變更會蛋疼到難以言喻?爲何首次進入瀏覽器建立兩張表再寫入數據會失效?等一系列問題。async

話很少說,先上代碼,下面是我對 IndexedDB 的簡單封裝用做講解。

class localDB {
    constructor(openRequest = {}, db = {}, objectStore = {}) {
        this.openRequest = openRequest;
        this.db = db;
        this.objectStore = objectStore;
        Object.getOwnPropertyNames(this.__proto__).map(fn => {
            if (this.__proto__[fn] === 'function') {
                this[fn] = this[fn].bind(this);
            }
        })
    }
    openDB(ops, version) {
        let db = Object.assign(new defaultVaule('db'), ops);
        this.openRequest = !!version ? window.indexedDB.open(db.name, version) : window.indexedDB.open(db.name);
    }
    onupgradeneeded() {
        const upgradeneed = new Promise((resolve, reject) => {
            this.openRequest.onupgradeneeded = (event) => {
                this.db = event.target.result;
                resolve(this);
            }
        })
        return upgradeneed;
    }
    onsuccess() {
        const success = new Promise((resolve, reject) => {
            this.openRequest.onsuccess = (event) => {
                this.db = event.target.result;
                resolve(this);
            }
        })
        return success;
    }
    createObjectStore(ops) {
        let list = Object.assign(new defaultVaule('list'), ops);
        const store = new Promise((resolve, reject) => {
            this.objectStore = this.db.createObjectStore(list.name, {
                keyPath: list.keyPath,
                autoIncrement: list.auto
            });
            resolve(this);
        })
        return store;
    }
    createIndex(ops, save) {
        const store = new Promise((resolve, reject) => {
            ops.map(data => {
                let o = Object.assign(new defaultVaule('idx'), data);
                this.objectStore.createIndex(o.name, o.name, {
                    unique: o.unique
                })
            })
            resolve(this);
        })
        return store;
    }
    saveData(type = {}, savedata) {
        let save = Object.assign(new defaultVaule('save'), type);
        const transAction = new Promise((resolve, reject) => {
            let preStore = this.objectStore = this.getObjectStore(save);
            preStore.transaction.oncomplete = (event) => {
                let f = 0;
                let store = this.objectStore = this.getObjectStore(save);
                savedata.map(data => {
                    let request = store.add(data);
                    request.onsuccess = (event) => {
                        // todo 這裏至關於每一個存儲完成後的回調,能夠作點其餘事,也能夠啥都不幹,反正留出來吧 :)
                    }
                    f++;
                })
                if (f == savedata.length) {
                    resolve(this);
                }
            }
        })
        return transAction;
    }
    getData(ops, name, value) {
        let store = this.getObjectStore(ops);
        let data = new Promise((resolve, reject) => {
            store.index(name).get(value).onsuccess = (event) => {
                event.target.result ? resolve(event.target.result) : resolve('暫無相關數據')
            }
        })
        return data;
    }
    getAllData(ops) {
        let store = this.getObjectStore(ops);
        let data = new Promise((resolve, reject) => {
            store.getAll().onsuccess = (event) => {
                event.target.result ? resolve(event.target.result) : resolve('暫無相關數據')
            };
        })
        return data;
    }
    deleteData(ops,name) { // 主鍵名
        let store = this.getObjectStore(ops);
        store.delete(name).onsuccess = (event) => {
            console.log(event);
            console.log(this);
        }
    }
    updateData(ops, index, lastValue, newValue) { // index 索引名 lastValue 須要修改的值 newValue 修改後的值
        let store = this.getObjectStore(ops);
        let data = new Promise((resolve, reject) => {
            store.openCursor().onsuccess = (event) => {
                const cursor = event.target.result;
                if (cursor) {
                    if (cursor.value[index] == lastValue) {
                        let updateData = cursor.value;
                        updateData[index] = newValue;
                        let updateDataRequest = cursor.update(updateData)
                        updateDataRequest.onsuccess = () => {
                            resolve('更新完成');
                        };
                    }
                    cursor.continue();
                } else {
                    resolve('找不到指定的值');
                }
            }
        })
        return data;
    }
    getObjectStore(ops) {
        return this.db.transaction(ops.name, ops.type).objectStore(ops.name);
    }
    clear(ops) {
        let clear = new Promise((resolve, reject) => {
            this.getObjectStore(ops).clear();
            resolve(this);
        })
        return clear
    }
    deleteStore(name) {
        let store = new Promise((resolve, reject) => {
            this.db.deleteObjectStore(name);
            resolve(this);
        })
        return store;
    }
    updateDB() {
        let version = this.db.version;
        let name = this.db.name;
        let update = new Promise((resolve, reject) => {
            this.closeDB();
            this.openDB({
                name: name
            }, ++version);
            resolve(this);
        })
        return update;
    }
    closeDB() {
        this.db.close();
        this.objectStore = this.db = this.request = {};
    }
}

class defaultVaule {
    constructor(fn) {
        if (typeof this.__proto__[fn] === 'function') {
            return this.__proto__[fn]();
        }
    }
    db() {
        return {
            name: 'myDB',
        }
    }
    list() {
        return {
            name: 'myList',
            keyPath: 'id',
            auto: false,
        }
    }
    idx() {
        return {
            name: 'myIndex',
            unique: false,
        }
    }
    save() {
        return {
            name: 'myList',
            type: 'readwrite'
        }
    }
}

模擬一下用戶在使用的時候遇到的場景:

一、打開瀏覽器 → 由於是首次進入瀏覽器,這時必然觸發 onsuccess 與 onupgradeneeded 。此時咱們在 onupgradeneeded 中建表創建索引,存入或者不存入初始數據之類的操做,固然仍是根據具體的業務邏輯來。

let db = new localDB();

db.openDB(DB);

db.onsuccess().then(data => {
    console.log('onsuccess');
    // todo
})

db.onupgradeneeded().then(data => {
    console.log('onupgradeneeded');
    // todo
})

此處,若是隻創建一張表,再存入數據,那寫法多是多樣的,例如

db.onupgradeneeded().then(data => {
    data.createObjectStore(MAINKEY).then(data => {
        data.createIndex(ITEMKEY).then(data => {
            console.log('表和索引建立完畢')
        })
    })
})

db.onsuccess().then(data=>{
    data.saveData(SAVETYPE, person).then(data => {
        console.log('數據寫入完畢');
    })
})

這樣作,不是不能夠,可是沒有必要,既然用了promise就不要搞成無限嵌套 推薦使用 async/await 看上去更美滋滋。

async function showDB(db) {
    try {
        await db.createObjectStore(MAINKEY);
        await db.createIndex(ITEMKEY);
        return db;
    } catch (err) {
        console.log(err);
    }
}

db.onupgradeneeded().then(data => {
    console.log('onupgradeneeded');
    showDB(data).then(data=>{
        console.log('表以及索引建立完畢')
    })
})

用同步寫異步,邏輯層面更清晰一點。上述代碼其實迴歸本質依然是

var localIDB = function() {
    this.request = this.db = this.objectStore = {};
}

localIDB.prototype = {
    openDB: function(ops, callback) {
        var ops = this.extend(ops, this.defaultDB());
        this.request = window.indexedDB.open(ops.name, ops.version);
        return this;
    },
    onupgradeneeded: function(callback) {
        var _this = this;
        this.request.onupgradeneeded = function(event) {
            _this.db = event.target.result;
            callback && callback(event, _this);
        }
        return this;
    },
    onsuccess: function(callback) {
        var _this = this;
        this.request.onsuccess = function(event) {
            _this.db = event.target.result;
            callback && callback(event, _this);
        }
        return this;
    }
}

var db = new localDB();
db.open().onupgradeneeded(function(event,data){
    // todo event是這個事件,data指向對象自己
}).onsuccess(function(event,data){
   // todo 同上
})

其實看上去差很少對不對,但若是創建兩張表,並分別寫入數據呢? async/await 就顯得更清晰了

async function showDB(db) {
    try {
        await db.createObjectStore(MAINKEY); 
        await db.createIndex(ITEMKEY);
        let success = await db.onsuccess();     // 第一次 觸發 onsuccess
        await success.saveData(SAVETYPE, person); // 第一次 寫入數據
        await success.updateDB(); // 升級數據庫 
        await success.onupgradeneeded(); // 第二次 觸發 onupgradeneeded
        await success.createObjectStore(MAINKEY1); 
        await success.createIndex(ITEMKEY1);
        let success1 = await success.onsuccess(); // 第二次 觸發 onsuccess
        await success1.saveData(SAVETYPE1, personDetail); // 第二次 寫入數據
        return success1;
    } catch (err) {
        console.log(err);
    }
}

db.onupgradeneeded().then(data => {
    console.log('onupgradeneeded');
    showDB(data).then(data => {
        console.log('兩張表,分別寫入數據完成');
    })
})

db.onsuccess().then(data=>{
     console.log('數據庫加載完畢');
})
這裏有個值得注意的地方:
  1. 當用戶第一次進入時開庫建表觸發的是 onupgradeneeded 以及完成開庫建表操做的 onsuccess 。實際狀況也確實如此,但咱們在 onupgradeneeded 裏面執行了函數 showDB(),因而問題來了:

    • 那麼,showDB()的返回是什麼呢?
    • 答:執行了saveData的對象db自己。
    • 爲何最外層的

      db.onsuccess().then(data=>{
       console.log('數據庫加載完畢');
      })

      沒有被觸發呢?

    • 答:async/await 中第一個 onsuccess 的 callback 用來執行寫入操做以及以後的升級,第二次建表等等。通俗的來說大概就是:這是一個異步的且連貫的操做,外層的 onsuccess 根本沒機會插手的機會。
  2. 當用戶第二次進入時(刷新頁面之列的操做),由於版本號沒有變化因此只會觸發 onsuccess 。 這個時候就會觸發最外層的 onsuccess 了。
讓咱們舉一個簡單的查詢例子:

按照上文,咱們已經有一個數據庫
表1:
1.png
表2:
2.png

假設:咱們須要從表1中拿到秀兒的uid,而後用uid去表2中獲取秀兒的具體信息。

// html部分代碼
    <button onclick="getXiuer()"></button>
    // js 部分
    // 能夠以下嵌套的方式
    function getXiuer() {
        let uid;
        let obj;
        db.getData({
            name: 'person',
            type: 'readonly',
        }, 'name', '秀兒').then(data => {
            console.log(data)
            uid = data.uid;
            db.getData({
                name: 'detail',
                type: 'readonly',
            }, 'uid', uid).then(data => {
                console.log(data);
            });
        });
    }
    // 也能夠以下async/await的方式
    funtion getXiuer() {
        getXiuerWait(db).then(data => {
            console.log(data);
        })
    }
    async function getXiuerWait(db) {
        try {
            let uid;
            let data = await db.getData({
                name: 'person',
                type: 'readonly',
            }, 'name', '秀兒');
            let result = await db.getData({
                name: 'detail',
                type: 'readonly',
            }, 'uid', data.uid);
            return result;
        } catch (err) {
            console.log(err);
        }
    }

結果如圖所示:
3.png

獲取全部數據的返回值是一個數組

db.getAllData({
        name: 'detail',
        type: 'readonly'
    }).then(data => {
        console.log(data)
    })

如圖所示:
4.png

想必聰明的你已經發現,其實存入數據庫的值能夠是多種多樣的,字符串、數字、布爾值、數組都是能夠的。長度其實也沒有特別的限制(反正我連base64的本地圖片都存了 o(╥﹏╥)o )

假設:咱們須要修改一個已經存在的值(把索引爲 age 的值由 60 改成 17)

db.updateData(SAVETYPE1, 'age', 60, 17).then(data => {
        console.log(data)
    })

結果如圖所示:
6.png
5.png

總結

IndexedDB只要理清楚開篇的幾個概念即:

  • 數據庫: IDBDatabase
  • 對象倉庫:IDBObjectStore
  • 索引: IDBIndex
  • 事務: IDBTransaction
  • 操做請求:IDBRequest
  • 指針: IDBCursor

以及異步返回的時機,此時此刻在操做哪張表,能夠觸發哪幾個函數,實際上是一個蠻好用的工具。

如今再來回答 索引的修改應該如何進行?

答:

  1. 要麼在一開始就設計好索引,避免修改(這是句廢話 (ಥ﹏ಥ))
  2. 若是無可避免,那麼能夠備份當前索引(getAllData裏應有盡有)。再經過升級數據庫版本觸發 onupgradeneeded 刪除之前的表,建立新的表。然而這裏又有一個隱晦的坑 o(╥﹏╥)o

    • 若是用戶刷新頁面,也就是說僅觸發 onsuccess 。那麼,天然要升級一次版本號,在此次升級中觸發的 onupgradeneeded 中,讓咱們來看看索引的創建
    var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
    objectStore.createIndex("email", "email", { unique: true });
    • objectStore 也就是 IDBObjectStore 對象的獲取是經過創立主鍵來達成的。
    • 或者objectStore 也能夠經過事物 IDBTransaction 來獲取。
    • 但這裏有個問題 IDBTransaction 儘可能在 onsuccess 中,而主鍵建立在 onupgradeneeded 中,僵住了...

因此咱們的代碼可能看上去可能應該是這樣

// 懶得寫 async/await 版本的了 !!(╯' - ')╯︵ ┻━┻ 好累!反正就這意思
    db.updateDB().then(data => {
        data.onupgradeneeded().then(data => {
            data.deleteStore('detail').then(data => {
                console.log(data);
                // 建表 建包含新索引的索引 再存入數據
            })
        })
    })

看到了嗎?這是人乾的事兒嗎?第一次開庫建表的時候就能夠弄好的事情,不要搞成這樣...

差很少就是這樣了,當只有1張表的時候,事情很輕鬆,可是多張表的時候笑容漸漸變態...

好了,有啥不清楚的,能夠留言,若是看到了,並且我會的話,確定會回答。

最後附上用做存儲的測試數據 (能夠忽略)

const DB = {
    name: 'student',
    version: 1
}

const MAINKEY = {
    name: 'person',
    keyPath: 'id',
    auto: true,
}

const ITEMKEY = [{
    name: 'name',
    unique: false,
}, {
    name: 'uid',
    unique: true,
}]

const person = [{
    name: '秀兒',
    uid: '100',
}, {
    name: '張三',
    uid: '101',
}, {
    name: '李敏',
    uid: '102',
}, {
    name: '日天',
    uid: '103',
}]

const SAVETYPE = {
    name: 'person',
    type: 'readwrite',
}

const MAINKEY1 = {
    name: 'detail',
    keyPath: 'uid',
    auto: false,
}

const ITEMKEY1 = [{
    name: 'uid',
    unique: false,
}, {
    name: 'age',
    unique: false,
}, {
    name: 'sex',
    unique: false,
}, {
    name: 'desc',
    unique: false,
}, {
    name: 'address',
    unique: false,
}]

const personDetail = [{
    uid: '102',
    age: '18',
    sex: '♀',
    desc: '女裝大佬',
    address: ["遙遠的地方"],
}, {
    uid: '103',
    age: '18',
    sex: 'man',
    desc: 'rua!',
    address: '{"test":"123","more":"asd"}',
}, {
    uid: '100',
    age: 'unknown',
    sex: 'unknown',
    desc: '666',
    address: true,
}, {
    uid: '101',
    age: 60,
    sex: 'man',
    desc: '路人甲',
    address: true,
}]

const SAVETYPE1 = {
    name: 'detail',
    type: 'readwrite',
}
相關文章
相關標籤/搜索