简介 IndexedDB 及详解 IndexedDB 在实际项目中可能存在的问题与解决方案

简介IndexedDB
详细文档请参看 MDN 文档链接
IndexedDB能做什么:
  • 它真的很能存东西! 对比cookie, local storeage, session storage 等受到大小限制的web存储方式, IndexedDB在理论上并无大小限制只与本地的磁盘相关。 这也是选择它作为web本地存储工具最大的理由。
  • 完整的API文档( 虽然大部分是英文Orz), 不懂的直接翻文档。 3. 异步, 这意味着不会锁死浏览器, 也意味着在进行多库多表操作时会很麻烦, 下文中会详细介绍。

废话不多说, 让我们直接用一个实际的例子来看 IndexedDB 如何使用。( PS: 这一部分算是对 IndexedDB 的简介和科普, 本文真正的核心在后面, 如果不想看科普可以直接跳到后面)

IndexedDB 需要理解以下几个重要的概念:
  • 数据库: IDBDatabase
  • 对象仓库: IDBObjectStore( 我更愿意称之为: 表)
  • 索引: IDBIndex
  • 事务: IDBTransaction
  • 操作请求: IDBRequest
  • 指针: IDBCursor

实际项目中一般正常的流程为:

开库→ 建表→ 创建索引→ 存入 / 删除数据→ 获取数据

这里我们先使用文档中的一个例子( 后面再来说哪里存在问题)

  const dbName = "the_name";

  const customerData = [{
    ssn: "444-44-4444",
    name: "Bill",
    age: 35,
    email: "[email protected]"
  }, {
    ssn: "555-55-5555",
    name: "Donna",
    age: 32,
    email: "[email protected]"
  }];

  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。
    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个问题所引发更多的小问题。

这个问题就是:多库或多表的同时操作。 (这也是本文真正的想要表达的东西)

在实际项目中, 不太可能一张表就写完所有数据, 有过数据库操作经验的老哥应该明白。 通常我们需要关联两张甚至多张表, 即一张表的键值, 是另一张表的键或主键, 所以我们可以关联这两张表, 而不必要也不需要在一张表里写完所有数据。

由于 IndexedDB 是异步实现, 所以首先要明确我们究竟在操作哪张表, 建立了哪个事物, 这个链接完成了吗? 等等。

明确上述问题才能解决: 为何索引变动会蛋疼到难以言喻? 为什么首次进入浏览器创建两张表再写入数据会失效? 等一系列问题。

话不多说, 先上代码, 下面是我对 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'
        }
    }
}

模拟一下用户在使用的时候遇到的场景:

1、 打开浏览器→ 因为是首次进入浏览器, 这时必然触发 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('数据库加载完毕');
})
这里有个值得注意的地方:
  • 当用户第一次进入时开库建表触发的是 onupgradeneeded 以及完成开库建表操作的 onsuccess。 实际情况也确实如此, 但我们在 onupgradeneeded 里面执行了函数 showDB(), 于是问题来了:
  • 问:那么, showDB() 的返回是什么呢?
  • 答: 执行了saveData的对象db本身。
  • 问:为什么最外层的 db.onsuccess().then(data => { console.log('数据库加载完毕'); }) 没有被触发呢?
  • 答:
    • async / await 中第一个 onsuccess 的 callback 用来执行写入操作以及之后的升级, 第二次建表等等。
    • 通俗的来讲大概就是: 这是一个异步的且连贯的操作, 外层的 onsuccess 根本没机会插手的机会。
    • 当用户第二次进入时( 刷新页面之列的操作), 因为版本号没有变化所以只会触发 onsuccess。 这个时候就会触发最外层的 onsuccess 了。
让我们举一个简单的查询例子:

按照上文, 我们已经有一个数据库
表1:
在这里插入图片描述
表2:
在这里插入图片描述

假设: 我们需要从表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);
    }
}

结果如图所示:
简介 IndexedDB 及详解 IndexedDB 在实际项目中可能存在的问题与解决方案_第1张图片
获取所有数据的返回值是一个数组

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

如图所示:
简介 IndexedDB 及详解 IndexedDB 在实际项目中可能存在的问题与解决方案_第2张图片
想必聪明的你已经发现, 其实存入数据库的值可以是多种多样的, 字符串、 数字、 布尔值、 数组都是可以的。 长度其实也没有特别的限制( 反正我连base64的本地图片都存了 o(╥﹏╥) o)

假设: 我们需要修改一个已经存在的值( 把索引为 age 的值由 60 改为 17)

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

结果如图所示:
简介 IndexedDB 及详解 IndexedDB 在实际项目中可能存在的问题与解决方案_第3张图片
在这里插入图片描述

总结

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',
}

你可能感兴趣的:(indexedDB,javascript,es6,前端)