废话很少说,让咱们直接用一个实际的例子来看 IndexedDB 如何使用。(PS:这一部分算是对 IndexedDB 的简介和科普,本文真正的核心在后面,若是不想看科普能够直接跳到后面)html
数据库: 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(库名,数据库版本)
一般来讲咱们常常会用到的函数 onsuccess : 成功回调,通俗的讲就是:你能够开始页面的其余操做了。 onupgradeneeded :升级数据库回调,通俗的讲就是:稳一手,再操做。
当仅当数据库版本号 发生变化的时候触发 onupgradeneeded 。换句话说,若是当前版本号为2。promise
event.target.result.createObjectStore('myList',{ keyPath: 'id', autoIncrement: true })
这个对象就是一般意义上的数据库自己,咱们能够经过这个对象进行表的增、删,以及事物 IDBTransaction 。
在IndexedDB中所作的全部事情老是发生在事务的上下文中,表示与数据库中的数据的交互。 IndexedDB中的全部对象——包括对象存储、索引和游标等都与特定事务绑定。 所以,在事务以外不能执行命令、访问数据或打开任何东西。 (PS: 通俗的意义上讲就是...此路是我开,此树是我栽,要想读写数据,请过我这关  ̄□ ̄ )
objectStore.createIndex("name", "name", { unique: false });
这个就是表了,它所包含的方法不少都是实际项目中常常用到的好比: add() 写入数据 createIndex() 建立索引 delete() 删除键 index() 获取索引 get() 检索值 getAll() 检索全部的值 不作过多叙述,详见文档。
还记得 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('数据库加载完毕'); })
当用户第一次进入时开库建表触发的是 onupgradeneeded 以及完成开库建表操做的 onsuccess 。实际状况也确实如此,但咱们在 onupgradeneeded 里面执行了函数 showDB(),因而问题来了:
为何最外层的
db.onsuccess().then(data=>{ console.log('数据库加载完毕'); })
没有被触发呢?
按照上文,咱们已经有一个数据库
表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); } }
结果如图所示:
获取全部数据的返回值是一个数组
db.getAllData({ name: 'detail', type: 'readonly' }).then(data => { console.log(data) })
如图所示:
想必聪明的你已经发现,其实存入数据库的值能够是多种多样的,字符串、数字、布尔值、数组都是能够的。长度其实也没有特别的限制(反正我连base64的本地图片都存了 o(╥﹏╥)o )
假设:咱们须要修改一个已经存在的值(把索引为 age 的值由 60 改成 17)
db.updateData(SAVETYPE1, 'age', 60, 17).then(data => { console.log(data) })
结果如图所示:
IndexedDB只要理清楚开篇的几个概念即:
以及异步返回的时机,此时此刻在操做哪张表,能够触发哪几个函数,实际上是一个蛮好用的工具。
如今再来回答 索引的修改应该如何进行?
答:
若是无可避免,那么能够备份当前索引(getAllData里应有尽有)。再经过升级数据库版本触发 onupgradeneeded 删除之前的表,建立新的表。然而这里又有一个隐晦的坑 o(╥﹏╥)o
var objectStore = db.createObjectStore("customers", { keyPath: "ssn" }); objectStore.createIndex("email", "email", { unique: true });
因此咱们的代码可能看上去可能应该是这样
// 懒得写 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', }