针对以下场景,在不使用库的情况下,如何使用indexedDB数据库, 下面将介绍使用直接使用indexedDB的痛点, 最后给出解决方案
一张数据表,大家应该都能够操作,但你会发现,只是简简单单的使用api,会出现一些意外情况,怎么解决这样的意外情况呢?请看下面的小demo
Document id:
str:
但是当你删除这张表, 刷新这个页面, 再次打开时,你就会发现不能正常进行操作了
Uncaught DOMException: Failed to execute ‘transaction’ on ‘IDBDatabase’: One of the specified object stores was not found.
why?
这是简单的操作,而且也经常用到,原因就是version
每次open一个数据库,如果需要操作数据表,就必须要在onupgradeneeded回调里面执行,但是只有version发生变化,才会再次调用onupgradeneeded
上面的原因就是删除表之后,改变了原来的version
下次再次打开, 就必须要重新建表,而建表的操作是需要onupgradeneeded,而onupgradeneeded回调执行,需要version再次发生变化
也就是初次建表,version为1,删除表, version为2
当再次建立表. 需要version为3,才会执行onupgradeneeded回调,重新见表
但是这个version一个变量不可以保存到在页面上,页面上的变量会随着页面刷新而被垃圾回收,
如何保存这个version下次还能再次使用且不影响下次页面重新建表
如何解决下面会讲到,先讲完所有案例会出现的意外情况先
当存在同一个数据库,两个表时, 同时要初始化两个表
当删除表之后,下次再次打开,进行数据库增删改查操作,出错, 版本不对
Document
tableA
id:
str:
tableB
id:
str:
报错依然很眼熟,没有重新建表,就去使用这个表了, indexedDB中,表就是store
Uncaught DOMException: Failed to execute ‘transaction’ on ‘IDBDatabase’: One of the specified object stores was not found.
这个跟上面的错误原因是一样的
并且删除表的时候,也是报错version的原因
DOMException: Version change transaction was aborted in upgradeneeded event handler.
当存在不同数据库, 不同表, 初始化不可以共用, 且数据库不同, 所以争对不同的数据库操作也是根据数据库来的
当删除表之后,下次再次打开,进行数据库增删改查操作,出错, 版本不对
Document
baseA, tableA
id:
str:
baseB, tableB
id:
str:
看到这里, 大家会不会觉得每次删除数据库之后, 操作就会出现问题,那不删除数据表, 是不是就没有问题了?
确实没有删除表, 数据就能下次继续这样操作, 那是不是就可以这样封装了呢?
等等, 还有一种情况
这是什么操作呢?
就是页面一初始化完成, 就立刻往数据表里面新增内容, 或者立刻查询数据表, 这个时候会出现什么情况呢?
请看下面例子:
Document id:
str:
Uncaught TypeError: Cannot read properties of null (reading ‘transaction’)
报错说dataBase为null
我们查看一下MDN文档里面是怎么说的
https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API
简单的情况,看起来很复杂, 其实就是说想要直接使用, 步骤比较多, 遇到的问题也很多
我们继续看, 如何使用indexedDB
https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API/Using_IndexedDB
IndexedDB 鼓励使用的基本模式如下所示:
这些基本模式, 我们通过上面的例子, 也已经非常清楚和了解了
这才是核心, 所有操作都是异步的, 这也很好解释了, 上面的报错, 就是因为dataBase还未初始化完毕, 所以导致的错误
只有当open的success回调执行完,dataBase才算真正初始化完成
实现增删改查,实现上面这多种操作,复杂操作,例如阻塞未实现
首先我们考虑一下能否使用类来实现?一个数据表对应一个对象
异步回调,需要使用到promise,使用时可以用then继续回调,或者用async和await关键字
接下来,我们考虑初始化一张表需要执行的操作
然后就可以开始封装
我们初始化一张表格,需要哪些参数:
数据库名称(baseName)
数据表名称(sheetName)
主键(keyName)
索引(indexArr 数组)
version(版本)
直接看完整封装
export default class IndexedDB {dataBase = null; // 数据库dataBaseName = null; // 数据库名dataSheetName = null; // 数据表名keyName = '' // 主键indexArr = [] // 索引version = nullconstructor({baseName,sheetName,keyName,indexArr,version}) {// baseName: 数据库名称// sheetName:数据表名称// keyName:主键// indexArr: Array 索引<索引名, 索引是否重复(为true则表示同一个项的值不能重复)>this.dataBaseName = baseNamethis.dataSheetName = sheetNamethis.keyName = keyNamethis.indexArr = indexArrthis.version = version}isReady(version) {return new Promise((resolve, reject) => {// 打开数据库const request = window.indexedDB.open(this.dataBaseName, version || this.version)request.onerror = (event) => {console.error('dataBase打开报错', event.target.error)reject(event.target.error)}request.onsuccess = (event) => {this.dataBase = event.target.resultthis.version = event.target.result.versionconsole.info('dataBase打开成功', event.target.result)resolve()}// 新建数据表request.onupgradeneeded = (event) => {this.dataBase = event.target.resultlet objectStore = nullif (!this.dataBase.objectStoreNames.contains(this.dataSheetName)) { // 不存在该数据表console.info('dataBase建表')objectStore = this.dataBase.createObjectStore(this.dataSheetName, {keyPath: this.keyName})// 新建索引this.indexArr.forEach(item => {objectStore.createIndex(item.name, item.name, {unique: item.unique || false})})}}})}// 写数据操作write(obj) {return new Promise((resolve, reject) => {// put()有则更新,无则新增const request = this.dataBase.transaction(this.dataSheetName, 'readwrite').objectStore(this.dataSheetName).put(obj)request.onsuccess = () => {console.info('write data success')resolve(this)}request.onerror = (e) => {console.error('write data fail')reject(e.target.error)}})}// 新增数据add(obj) {return new Promise((resolve, reject) => {if (this.dataBase.objectStoreNames.contains(this.dataSheetName)) { // 确定有表this.write(obj).then(() => {resolve()}).catch(err => {console.error(err);reject(err)})} else { // 如果表未成功建立// 第一步断开库连接this.dataBase.close()// 新增版本(必须新增版本,否则不会执行onupgradeneeded, 而表操作必须在onupgradeneeded回调中执行)this.version++;// 第二步// 重新建表this.isReady().then(() => {// 重新写入数据this.write(obj).then(() => {resolve()})}).catch(err => {console.error(err);reject(err)})}})}// 更新数据update(obj) {// 没有该索引就新增,有就更新return new Promise((resolve, reject) => {this.add(obj).then(() => {resolve()}).catch((e) => {reject(e)})})}// 读取数据read(key) {// throw new Error('xxxxxxxxx')return new Promise((resolve, reject) => {// throw new Error('1111111')if (!this.dataBase.objectStoreNames.contains(this.dataSheetName)) {return resolve('未建表')}const objectStore = this.dataBase.transaction([this.dataSheetName]).objectStore(this.dataSheetName)const request = objectStore.get(key)request.onerror = (e) => {console.error('read dataBase fail')reject(e.target.error)}request.onsuccess = () => {if (request.result) {resolve(request.result)} else {console.info('not find any data')resolve(null)}}})}// 删除数据remove(key) {return new Promise((resolve, reject) => {// 不管有没有这个索引,都是删除成功的回调if (!this.dataBase.objectStoreNames.contains(this.dataSheetName)) {return resolve("未建该表")}const request = this.dataBase.transaction([this.dataSheetName], 'readwrite').objectStore(this.dataSheetName).delete(key)request.onsuccess = (e) => {console.info('dataBase数据删除成功', e)resolve()}request.onerror = (e) => {console.error('dataBase数据删除事务失败')reject(e.target.error)}})}// 删除数据库deleDataBase(dataBaseName) {return new Promise((resolve, reject) => {const DBDeleteRequest = window.indexedDB.deleteDatabase(dataBaseName);this.dataBase.close();DBDeleteRequest.onerror = (event) => {console.error("Error deleting database.");reject(event.target.error)};DBDeleteRequest.onsuccess = (event) => {console.log("Database deleted successfully", event);console.log(event.result); // should be undefinedresolve(event.result)};})}// 删除表deleSheet(version) {return new Promise((resolve, reject) => {const request = window.indexedDB.open(this.dataBaseName, version);request.onerror = (event) => {// Handle errors.console.error(event.target.error)reject(event.target.error)};request.onsuccess = (event) => {this.dataBase.close();console.log('delet success')}request.onupgradeneeded = (event) => {this.dataBase = event.target.result;this.version = this.dataBase.version;this.dataBase.deleteObjectStore(this.dataSheetName);// this.dataBase.close(); //删除之后不需要再关闭,否则会报错 DOMException: The connection was closedconsole.log(event.target, 'delete success')resolve(event)};})}destroy() {this.dataBase.close();console.log('数据库连接关闭')}
}
1.为什么初始化时,不执行open数据库?
2.为什么isReady()可传参,可不传参,什么情况适合传参?
3.为什么新增和修改数据时,需要先判断是否有表?
4.为什么删除和查询,不需要再次更新版本
接下来通过demo来解释
操作一张表
student表
id:
str:
1.为什么初始化时,不执行open数据库?
通过这个demo,可以解释第一个疑问,没有执行open,是为了每次操作时就执行isReady()操作,也就是说只要进行数据表的增删改查再去新建这张表,这样就不存在,未建表时,进行表操作;如果这张表已经建好了,那自然而然不会执行onupgradeneeded回调
每次新增完这张数据表,就可以关闭数据库连接,只要不频繁的进行数据库的增删改查操作,我觉得就能关闭数据库连接,以免忘记关闭,同时也防止出现多次open数据库的情况
多次open数据库,但是没有及时close,会导致删除表操作时,卡死
删除表时,我们知道,需要版本号进行修改,且执行onupgradeneeded回调
也就是说多次open数据库,没有依次进行close,会导致真正的version发生改变时,也不执行onupgradeneeded回调
同一个页面操作同一个数据库中的不同表
student表
id:
str:
teacher表
id:
str:
效果如图
2.为什么isReady()可传参,可不传参,什么情况适合传参?
这里可以解释这个疑问,同一个数据库中,多张表操作,会导致version版本号发生变化,所以需要页面维护这个版本号变量,当从一个表创建好之后,再创建另一个表时,需要升级版本号
所以当存在一个页面出现同一个数据库,多张表的情况就需要维护这个最高的版本号是多少了
当然,刷新页面这个版本号这个变量就会销毁,但是没关系,我们每次isReady()的时候,能够读取到最新的version变量
当一个页面操作同一个数据库中的不同表时,就需要维护version,所以也就这种情况需要传参,不然拿不到最新的version值,就会出现如下报错
dataBase打开报错 DOMException: The requested version (11) is less than the existing version (12).
并且删除数据库之后,下次依旧还能继续创建数据表再次进行数据添加和删除操作
同一个页面操作不同数据库中的不同表
ss库中student表
id:
str:
tt库teacher表
id:
str:
3.为什么新增和修改数据时,需要先判断是否有表?
因为当open表的回调还未执行完成, 可能页面一初始化完成,就立刻去查询页面是否存在某个数据, 当不存在这个数据时, 再去新增这个数据, 这张情况下, 就会出现未成功建表, 可以使用dataBase.objectStoreNames.contains(this.dataSheetName)判断
如果未建表, 就去操作数据表, 肯定是会报错的, 错误如下:
DOMException: Failed to execute ‘transaction’ on ‘IDBDatabase’: One of the specified object stores was not found.
数据库未open连接报错如下:
DOMException: Failed to execute ‘transaction’ on ‘IDBDatabase’: The database connection is closing.
4.为什么删除和查询,不需要再次更新版本
能够发现, 新增和更新, 本质都是调用add方法, 而add方法中, 判断未建表时, 会重新建表
原因其实也很简单, 新增数据, 那说明这个数据需要新增或者更新, 那这个时候未建表, 肯定会报错, 但是如果一直未建表, 就一直不能新增数据, 这就会导致功能出错了, 所以这里做了一点特殊处理.
Document
tableA
id:
str:
tableB
id:
str:
最后附上本文的代码仓库:
https://gitee.com/tiantianhy/indexedBD
当然如果还需要满足阻塞等更加复杂的操作,推荐使用indexedDB的库,这里我推荐idb,参考文章中有介绍idb库使用的文章
参考文章:
英文:
https://hackernoon.com/use-indexeddb-with-idb-a-1kb-library-that-makes-it-easy-8p1f3yqq中文: https://blog.csdn.net/ioriogami/article/details/128438731
https://zh.javascript.info/indexeddb#da-kai-shu-ju-ku