indexedDB
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据,它可以被网页脚本创建和操作。 IndexedDB 允许储存大量数据,提供查找接口,还能建立索引,这些都是 LocalStorage 所不具备的。 就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。 其他的介绍就不搬运了,大家可以自行百度,后面有参考资料。
需求
我想更好的实现文档驱动的想法,发现需要实现前端存储的功能,于是打算采用 IndexedDB 来实现前端存储的功能。但是看了一下其操作方式比较繁琐,所以打算封装一下。
官网给了几个第三方的封装库,我也点过去看了看,结果没看懂。想了想还是自己动手丰衣足食吧。
关于重复制造轮子的想法:
- 首先要有制造轮子能能力。
- 自己造的轮子,操控性更好。
功能设计
按照官网的功能介绍,把功能整理了一下: 如图:
就是建库、增删改查那一套。看到有些第三方的封装库,可以实现支持sql语句方式的查询,真的很厉害。目前没有这种需求,好吧,能力有限实现不了。 总之,先满足自己的需求,以后在慢慢改进。
代码实现
还是简单粗暴,直接上代码吧,基础知识的介绍,网上有很多了,可以看后面的参考资料。官网介绍的也比较详细,还有中文版的。
配置文件
nf-indexedDB.config
const config = { dbName: 'dbTest', ver: 1, debug: true, objectStores: [ // 建库依据 { objectStoreName: 'blog', index: [ // 索引 , unique 是否可以重复 { name: 'groupId', unique: false } ] } ], objects: { // 初始化数据 blog: [ { id: 1, groupId: 1, title: '这是一个博客', addTime: '2020-10-15', introduction: '这是博客简介', concent: '这是博客的详细内容<br>第二行', viewCount: 1, agreeCount: 1 }, { id: 2, groupId: 2, title: '这是两个博客', addTime: '2020-10-15', introduction: '这是博客简介', concent: '这是博客的详细内容<br>第二行', viewCount: 10, agreeCount: 10 } ] } } export default config 复制代码
- dbName
指定数据库名称
- ver
指定数据库版本
- debug
指定是否要打印状态
- objectStores
对象仓库的描述,库名、索引等。
- objects
初始化数据,如果建库后需要添加默认数据的话,可以在这里设置。
这里的设置不太完善,有些小问题现在还没想好解决方法。以后想好了再改。
内部成员
/** * IndexedDB 数据库对象 * 判断浏览器是否支持 * */ const myIndexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB if (!myIndexedDB) { console.log('你的浏览器不支持IndexedDB') } let _db // 内部保存的 indexed 数据库 的实例 /** * 把vue的ref、reactive转换成原始对象 */ const _vueToObject = (vueObject) => { let _object = vueObject // 针对Vue3做的类型判断 if (Vue.isRef(_object)) { // 如果是 vue 的 ref 类型,替换成 ref.value _object = _object.value } if (Vue.isReactive(_object)) { // 如果是 vue 的 reactive 类型,那么获取原型,否则会报错 _object = Vue.toRaw(_object) } return _object } 复制代码
- myIndexedDB
兼容浏览器的写法,适应不同的浏览器。
- _db 内部的 IDBOpenDBRequest 用于检查是否打开数据库,以及数据库的相关操作。
- _vueToObject
这是一个兼容Vue的对象转换函数。vue的reactive直接存入的话会报错,需要获取原型才能存入,我又不想每次保存的时候都多一步操作,所以就写了这个转换函数。 如果非vue3环境,可以直接返回参数,不影响其他功能。
建立对象库以及打开数据库
// ======== 数据库操作 ================ /** * 打开 indexedDB 数据库。 * dbName:数据库名称; * version:数据库版本。 * 可以不传值。 */ const dbOpen = (dbName, version) => { // 创建数据库,并且打开 const name = config.dbName || dbName const ver = config.ver || version const dbRequest = myIndexedDB.open(name, ver) // 记录数据库版本是否变更 let isChange = false /* 该域中的数据库myIndex */ if (config.debug) { console.log('dbRequest - 打开indexedDb数据库:', dbRequest) } // 打开数据库的 promise const dbPromise = new Promise((resolve, reject) => { // 数据库打开成功的回调 dbRequest.onsuccess = (event) => { // _db = event.target.result // 数据库成功打开后,记录数据库对象 _db = dbRequest.result if (isChange) { // 如果变更,则设置初始数据 setup().then(() => { resolve(_db) }) } else { resolve(_db) } } dbRequest.onerror = (event) => { reject(event) // 返回参数 } }) // 创建表 // 第一次打开成功后或者版本有变化自动执行以下事件,一般用于初始化数据库。 dbRequest.onupgradeneeded = (event) => { isChange = true _db = event.target.result /* 数据库对象 */ // 建立对象表 for (let i = 0; i < config.objectStores.length; i++) { const object = config.objectStores[i] // 验证有没有,没有的话建立一个对象表 if (!_db.objectStoreNames.contains(object.objectStoreName)) { const objectStore = _db.createObjectStore(object.objectStoreName, { keyPath: 'id' }) /* 创建person仓库(表) 主键 */ // objectStore = _db.createObjectStore('person',{autoIncrement:true});/*自动创建主键*/ // 建立索引 for (let i = 0; i < object.index.length; i++) { const index = object.index[i] objectStore.createIndex(index.name, index.name, { unique: index.unique }) } if (config.debug) { console.log('onupgradeneeded - 建立了一个新的对象仓库:', objectStore) } } } } // 返回 Promise 实例 —— 打开Indexed库 return dbPromise } 复制代码
这段代码有点长,因为有两个功能,一个是打开数据库,一个是创建数据库。
indexedDB 的逻辑是这样的,在open数据库的时候判断本地有没有数据库,如果没有数据库则触发 onupgradeneeded 事件,创建数据库,然后打开数据库。 如果有数据库的话,判断版本号,如果高于本地数据库,那么也会触发 onupgradeneeded 事件。所以open和 onupgradeneeded 就联系在了一起。
初始化对象
/** * 设置初始数据 */ const setup = () => { // 定义一个 Promise 的实例 const objectPromise = new Promise((resolve, reject) => { const arrStore = [] // 遍历,获取表名集合,便于打开事务 for (const key in config.objects) { arrStore.push(key) } const tranRequest = _db.transaction(arrStore, 'readwrite') // 遍历,添加数据(对象) for (const key in config.objects) { const objectArror = config.objects[key] const store = tranRequest.objectStore(key) // 清空数据 store.clear().onsuccess = (event) => { // 遍历添加数据 for (let i = 0; i < objectArror.length; i++) { store .add(objectArror[i]) .onsuccess = (event) => { if (config.debug) { console.log(`添加成功!key:${key}-i:${i}`) } } } } } // 遍历后统一返回 tranRequest.oncomplete = (event) => { // tranRequest.commit() if (config.debug) { console.log('setup - oncomplete') } resolve() } tranRequest.onerror = (event) => { reject(event) } }) return objectPromise } 复制代码
有的时候需要在建库之后设置一些初始化的数据,于是设计了这个函数。 setup会依据 nf-indexedDB.config 里的配置,把默认对象添加到数据库里面。
添加对象
基础的增删改查系列,不管是数据库还是对象库,都躲不开。
// ======== 增删改操作 =================================== /** * 添加对象。 * storeName:对象仓库名; * object:要添加的对象 */ const addObject = (storeName, object) => { const _object = _vueToObject(object) // 定义一个 Promise 的实例 const objectPromise = new Promise((resolve, reject) => { // 定义个函数,便于调用 const _addObject = () => { const tranRequest = _db.transaction(storeName, 'readwrite') tranRequest .objectStore(storeName) // 获取store .add(_object) // 添加对象 .onsuccess = (event) => { // 成功后的回调 resolve(event.target.result) // 返回对象的ID } tranRequest.onerror = (event) => { reject(event) } } // 判断数据库是否打开 if (typeof _db === 'undefined') { dbOpen().then(() => { _addObject() }) } else { _addObject() } }) return objectPromise } 复制代码
这么长的代码,只是实现了把一个对象填到数据库里的操作,可见原本的操作是多么的繁琐。
好吧,不开玩笑了,其实原本的想法是这样的,想要添加对象要这么写:
dbOpen().then(() =>{ addObject('blog',{ id: 3, groupId: 1, title: '这是三个博客', addTime: '2020-10-15', introduction: '这是博客简介', concent: '这是博客的详细内容<br>第二行', viewCount: 1, agreeCount: 1 }) }) 复制代码
就是说,每次操作的时候先开库,然后才能进行操作,但是想想这么做是不是有点麻烦? 能不能不管开不开库的,直接开鲁呢? 于是内部实现代码就变得复杂了一点。
修改对象
/** * 修改对象。 * storeName:对象仓库名; * object:要修改的对象 */ const updateObject = (storeName, object) => { const _object = _vueToObject(object) // 定义一个 Promise 的实例 const objectPromise = new Promise((resolve, reject) => { // 定义个函数,便于调用 const _updateObject = () => { const tranRequest = _db.transaction(storeName, 'readwrite') // 按照id获取对象 tranRequest .objectStore(storeName) // 获取store .get(_object.id) // 获取对象 .onsuccess = (event) => { // 成功后的回调 // 从仓库里提取对象,把修改值合并到对象里面。 const newObject = { ...event.target.result, ..._object } // 修改数据 tranRequest .objectStore(storeName) // 获取store .put(newObject) // 修改对象 .onsuccess = (event) => { // 成功后的回调 if (config.debug) { console.log('updateObject -- onsuccess- event:', event) } resolve(event.target.result) } } tranRequest.onerror = (event) => { reject(event) } } // 判断数据库是否打开 if (typeof _db === 'undefined') { dbOpen().then(() => { _updateObject() }) } else { _updateObject() } }) return objectPromise } 复制代码
修改对象,是新的对象覆盖掉原来的对象,一开始是想直接put,但是后来实践的时候发现,可能修改的时候只是修改其中的一部分属性,而不是全部属性,那么直接覆盖的话,岂不是造成参数不全的事情了吗?
于是只好先把对象拿出来,然后和新对象合并一下,然后再put回去,于是代码就又变得这么长了。
删除对象
/** * 依据id删除对象。 * storeName:对象仓库名; * id:要删除的对象的key值,注意类型要准确。 */ const deleteObject = (storeName, id) => { // 定义一个 Promise 的实例 const objectPromise = new Promise((resolve, reject) => { // 定义个函数,便于调用 const _deleteObject = () => { const tranRequest = _db.transaction(storeName, 'readwrite') tranRequest .objectStore(storeName) // 获取store .delete(id) // 删除一个对象 .onsuccess = (event) => { // 成功后的回调 resolve(event.target.result) } tranRequest.onerror = (event) => { reject(event) } } // 判断数据库是否打开 if (typeof _db === 'undefined') { dbOpen().then(() => { _deleteObject() }) } else { _deleteObject() } }) return objectPromise } 复制代码
其实吧删除对象,一个 delete 就可以了,但是还是要先判断一下是否打开数据库,于是代码还是短不了。
清空仓库里的对象
/** * 清空store里的所有对象。 * storeName:对象仓库名; */ const clearStore = (storeName) => { // 定义一个 Promise 的实例 const objectPromise = new Promise((resolve, reject) => { // 定义个函数,便于调用 const _clearStore = () => { const tranRequest = _db.transaction(storeName, 'readwrite') tranRequest .objectStore(storeName) // 获取store .clear() // 清空对象仓库里的对象 .onsuccess = (event) => { // 成功后的回调 resolve(event) } tranRequest.onerror = (event) => { reject(event) } } // 判断数据库是否打开 if (typeof _db === 'undefined') { dbOpen().then(() => { _clearStore() }) } else { _clearStore() } }) return objectPromise } 复制代码
- clear()
清空指定对象仓库里的所有对象,请谨慎操作。
删除对象仓库
/** * 删除整个store。 * storeName:对象仓库名; */ const deleteStore = (storeName) => { // 定义一个 Promise 的实例 const objectPromise = new Promise((resolve, reject) => { // 定义个函数,便于调用 const _deleteStore = () => { const tranRequest = _db.transaction(storeName, 'readwrite') tranRequest .objectStore(storeName) // 获取store .delete() // 清空对象仓库里的对象 .onsuccess = (event) => { // 成功后的回调 resolve(event) } tranRequest.onerror = (event) => { reject(event) // 失败后的回调 } } // 判断数据库是否打开 if (typeof _db === 'undefined') { dbOpen().then(() => { _deleteStore() }) } else { _deleteStore() } }) return objectPromise } 复制代码
这个就更厉害了,可以把对象仓库给删掉。更要谨慎。
删除数据库
/** * 删除数据库。 * dbName:数据库名; */ const deleteDB = (dbName) => { // 定义一个 Promise 的实例 const objectPromise = new Promise((resolve, reject) => { // 删掉整个数据库 myIndexedDB.deleteDatabase(dbName).onsuccess = (event) => { resolve(event) } }) return objectPromise } 复制代码
能建立数据库,那么就应该能删除数据库,这个就是。 这个就非常简单了,不用判断是否打开数据库,直接删除就好。 不过前端数据库应该具备这样的功能:整个库删掉后,可以自动恢复状态才行。
按主键获取对象,或者获取全部
/** * 获取对象。 * storeName:对象仓库名; * id:要获取的对象的key值,注意类型要准确,只能取一个。 * 如果不设置id,会返回store里的全部对象 */ const getObject = (storeName, id) => { const objectPromise = new Promise((resolve, reject) => { const _getObject = () => { const tranRequest = _db.transaction(storeName, 'readonly') const store = tranRequest.objectStore(storeName) // 获取store let dbRequest // 判断是获取一个,还是获取全部 if (typeof id === 'undefined') { dbRequest = store.getAll() } else { dbRequest = store.get(id) } dbRequest.onsuccess = (event) => { // 成功后的回调 if (config.debug) { console.log('getObject -- onsuccess- event:', id, event) } resolve(event.target.result) // 返回对象 } tranRequest.onerror = (event) => { reject(event) } } // 判断数据库是否打开 if (typeof _db === 'undefined') { dbOpen().then(() => { _getObject() }) } else { _getObject() } }) return objectPromise } 复制代码
这里有两个功能
- 依据ID获取对应的对象
- 获取对象仓库里的所有对象
不想取两个函数名,于是就依据参数来区分了,传递ID就获取ID的对象,没有传递ID就返回全部。
查询对象仓库
/** * 依据 索引+游标,获取对象,可以获取多条。 * storeName:对象仓库名。 * page:{ * start:开始, * count:数量, * description:'next' * // next 升序 * // prev 降序 * // nextunique 升序,只取一 * // prevunique 降序,只取一 * } * findInfo = { * indexName: 'groupId', * indexKind: '=', // '>','>=','<','<=','between', * indexValue: 1, * betweenInfo: { * v1:1, * v2:2, * v1isClose:true, * v2isClose:true, * }, * where:(object) => { * reutrn true/false * } * } */ const findObject = (storeName, findInfo = {}, page = {}) => { const _start = page.start || 0 const _count = page.count || 0 const _end = _start + _count const _description = page.description || 'prev' // 默认倒序 // 查询条件,按照主键或者索引查询 let keyRange = null if (typeof findInfo.indexName !== "undefined") { if (typeof findInfo.indexKind !== "undefined") { const id = findInfo.indexValue const dicRange = { "=":IDBKeyRange.only(id), ">":IDBKeyRange.lowerBound(id, true), ">=":IDBKeyRange.lowerBound(id), "<":IDBKeyRange.upperBound(id, true), "<=":IDBKeyRange.upperBound(id) } switch (findInfo.indexKind) { case '=': case '>': case '>=': case '<': case '<=': keyRange = dicRange[findInfo.indexKind] break case 'between': const betweenInfo = findInfo.betweenInfo keyRange = IDBKeyRange.bound(betweenInfo.v1,betweenInfo.v2,betweenInfo.v1isClose,betweenInfo.v2isClose) break } } } console.log('findObject - keyRange', keyRange) const objectPromise = new Promise((resolve, reject) => { // 定义个函数,便于调用 const _findObjectByIndex = () => { const dataList = [] let cursorIndex = 0 const tranRequest = _db.transaction(storeName, 'readonly') const store = tranRequest.objectStore(storeName) let cursorRequest // 判断是否索引查询 if (typeof findInfo.indexName === "undefined") { cursorRequest = store.openCursor(keyRange, _description) } else { cursorRequest = store .index(findInfo.indexName) .openCursor(keyRange, _description) } cursorRequest.onsuccess = (event) => { const cursor = event.target.result if (cursor) { if (_end === 0 || (cursorIndex >= _start && cursorIndex < _end)) { // 判断钩子函数 if (typeof findInfo.where === 'function') { if (findInfo.where(cursor.value, cursorIndex)) { dataList.push(cursor.value) cursorIndex++ } } else { // 没有设置查询条件 dataList.push(cursor.value) cursorIndex++ } } cursor.continue() } // tranRequest.commit() } tranRequest.oncomplete = (event) => { if (config.debug) { console.log('findObjectByIndex - dataList', dataList) } resolve(dataList) } tranRequest.onerror = (event) => { console.log('findObjectByIndex - onerror', event) reject(event) } } // 判断数据库是否打开 if (typeof _db === 'undefined') { dbOpen().then(() => { _findObjectByIndex() }) } else { _findObjectByIndex() } }) return objectPromise } 复制代码
打开指定的对象仓库,然后判断是否设置了索引查询,没有的话打开仓库的游标,如果设置了,打开索引的游标。 可以用钩子实现其他属性的查询。 可以分页获取数据,方法类似于mySQL的 limit。
功能测试
封装完毕,要写个测试代码来跑一跑,否则怎么知道到底好不好用呢。 于是写了一个比较简单的测试代码。
建立对象库
dbOpen().then(() =>{ // 建表初始化之后,获取全部对象 getAll() }) 复制代码
- dbOpen
打开数据库,同时判断是否需要建立数据库,如果需要的话,会根据配置信息自动建立数据库
然后我们按F12,打开Application标签,可以找到我们建立的数据库,如图:
我们可以看一下索引的情况,如图:
添加对象
addObject('blog',{ id: new Date().valueOf(), groupId: 1, title: '这是三个博客', addTime: '2020-10-15', introduction: '这是博客简介', concent: '这是博客的详细内容<br>第二行', viewCount: 1, agreeCount: 1 }).then((data) => { re.value = data getAll() }) 复制代码
- 仓库名
第一个参数是对象仓库的名称,目前暂时采用字符串的形式。
- 对象
第二个参数是要添加的对象,其属性必须有主键和索引,其他随意。
- 返回值
成功后会返回对象ID
点右键可以刷新数据,如图:
更新后的数据,如图:
修改对象
updateObject('blog',blog).then((data) => { re.value = data getAll() }) 复制代码
- 仓库名
第一个参数是对象仓库的名称,目前暂时采用字符串的形式。
- 对象
第二个参数是要修改的对象,属性可以不全。
- 返回值
成功后会返回对象ID
删除对象
deleteObject('blog',id).then((data) => { re.value = data getAll() }) 复制代码
- 仓库名
第一个参数是对象仓库的名称,目前暂时采用字符串的形式。
- 对象
第二个参数是要删除的对象的ID。
- 返回值
成功后会返回对象ID
清空仓库里的对象
clearStore('blog').then((data) => { re.value = data getAll() }) 复制代码
- 仓库名
第一个参数是对象仓库的名称,目前暂时采用字符串的形式。
- 返回值
成功后会返回对象ID
删除对象仓库
deleteStore('blog').then((data) => { re.value = data getAll() }) 复制代码
- 仓库名
第一个参数是对象仓库的名称,目前暂时采用字符串的形式。
- 返回值
成功后会返回对象ID
删除数据库
deleteDB('dbTest').then((data) => { re.value = data getAll() }) 复制代码
- 数据库名称
第一个参数是数据库的名称
查询功能
// 查询条件 const findInfo = { indexName: 'groupId', indexKind: '=', // '>','>=','<','<=','between', indexValue: 1, betweenInfo: { v1:1, v2:2, v1isClose:true, v2isClose:true, }, where: (object) => { if (findKey.value == '') return true let re = false if (object.title.indexOf(findKey.value) >= 0) { re = true } if (object.introduction.indexOf(findKey.value) >= 0) { re = true } if (object.concent.indexOf(findKey.value) >= 0) { re = true } return re } } const find = () => { findObject('blog', findInfo).then((data) => { findRe.value = data }) } 复制代码
- findInfo
查询信息的对象,把需要查询的信息都放在这里
- indexName
索引名称,可以不设置。
- indexKind
索引属性的查询方式,如果设置indexName,则必须设置。
- indexValue
索引字段的查询值
- betweenInfo
如果 indexKind = 'between' 的话,需要设置。
- v1
开始值
- v2
结束值
- v1isClose
是否闭合区间
- v2isClose
是否闭合区间
- where
钩子函数,可以不设置。 内部打开游标后,会把对象返回来,然后我们就可以在这里进行各种条件判断。
全部代码就不贴了,感兴趣的话可以去GitHub看。 贴一个折叠后的效果图吧:
就是先把相关的功能和在一起,写一个操作类,然后在setup里面应用这个类就可以了,然后写点代码把各个类关联起来即可。
这样代码好维护多了。
源码
在线演示
naturefwvue.github.io/nf-vue-cnd/…
参考资料
官网:developer.mozilla.org/zh-CN/docs/…
阮一峰的网络日志:www.ruanyifeng.com/blog/2018/0…
谦行: www.cnblogs.com/dolphinX/p/…