2021-02-04 18:44 金色海洋(jyk) 阅读(285) 评论(3) 编辑 收藏 html
indexedDBIndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据,它能够被网页脚本建立和操做。
IndexedDB 容许储存大量数据,提供查找接口,还能创建索引,这些都是 LocalStorage 所不具有的。
就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
其余的介绍就不搬运了,你们能够自行百度,后面有参考资料。前端
我想更好的实现文档驱动的想法,发现须要实现前端存储的功能,因而打算采用 IndexedDB 来实现前端存储的功能。可是看了一下其操做方式比较繁琐,因此打算封装一下。vue
官网给了几个第三方的封装库,我也点过去看了看,结果没看懂。想了想仍是本身动手丰衣足食吧。react
关于重复制造轮子的想法:git
按照官网的功能介绍,把功能整理了一下:
如图:github
就是建库、增删改查那一套。看到有些第三方的封装库,能够实现支持sql语句方式的查询,真的很厉害。目前没有这种需求,好吧,能力有限实现不了。
总之,先知足本身的需求,之后在慢慢改进。web
仍是简单粗暴,直接上代码吧,基础知识的介绍,网上有不少了,能够看后面的参考资料。官网介绍的也比较详细,还有中文版的。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: '这是博客的详细内容
第二行', viewCount: 1, agreeCount: 1 }, { id: 2, groupId: 2, title: '这是两个博客', addTime: '2020-10-15', introduction: '这是博客简介', concent: '这是博客的详细内容
第二行', viewCount: 10, agreeCount: 10 } ] } } export default config
dbName
指定数据库名称promise
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: '这是博客的详细内容
第二行', 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 }
/** * 删除整个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就返回所有。
/** * 依据 索引+游标,获取对象,能够获取多条。 * storeName:对象仓库名。 * page:{ * start:开始, * count:数量, * description:'next' * // next 升序 * // prev 降序 * // nextunique 升序,只取一 * // prevunique 降序,只取一 * } * findInfo = { * indexName: 'groupId', * indexKind: '=', // '>','>=',' { * 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), " { // 定义个函数,便于调用 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() })
而后咱们按F12,打开Application标签,能够找到咱们创建的数据库,如图:
咱们能够看一下索引的状况,如图:
addObject('blog',{ id: new Date().valueOf(), groupId: 1, title: '这是三个博客', addTime: '2020-10-15', introduction: '这是博客简介', concent: '这是博客的详细内容
第二行', 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: '=', // '>','>=',' { 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里面应用这个类就能够了,而后写点代码把各个类关联起来便可。
这样代码好维护多了。
小结功能不是很完善,目前是本身够用的程度。
原本想用纯js来写个使用方式的,可是发现仍是用vue写着方便,因而测试代码就变成了vue的形式。
https://github.com/naturefwvue/nf-vue-cnd/tree/main/cnd/LocalStore/IndexedDB
在线演示https://naturefwvue.github.io/nf-vue-cnd/cnd/LocalStore/IndexedDB/
参考资料官网:https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API
阮一峰的网络日志:http://www.ruanyifeng.com/blog/2018/07/indexeddb.html