简介 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',
}
相关文章
相关标签/搜索