手把手教你写一个IndexDB库
需要解决的问题
数据库连接是异步的
window.indexedDB.open() 是一个异步操作,在项目中,为了db使用的灵活性,很难保证对数据库操作时,db已经成功连接。也不可能将所有的操作都放在 success的回调中,所以需要预先存储所有对数据库的操作,并且在db成功连接后,再执行这些操作。
根据上面的想法我写出了以下代码
// 任务队列
const queue = []
// 假设我们有以下三个需要执行的任务
// add
// create
// delete
// 我们需要一个将任务放入任务队列的方法
const commit = (fn)=>{
queue.push((db)={
fn(db)
})
}
// 还需要一个方法执行任务队列
const exec = (db)=>{
let fn = null
while(queue.length) {
fn = queue.pop()
try {
fn(db)
} catch(e) {
console.log(e)
}
}
}
// 提交任务
commit(add)
commit(create)
commit(delete)
// 在db成功连接后执行
const request = window.indexedDB.open(name);
request.onsuccess = ()=>{
exec(db)
}
上面就实现了先收集所有要执行的任务,并在数据库连接成功后执行,但新问题又接踵而至,我们无法获取任务执行的结果
如何正确获取执行结果
利用Promise resolve的方式来异步获取执行结果与错误
针对上面代码的commit方法进行一点改造
const commit = (fn)=>{
return new Promise((resolve, reject)=>{
queue.push((db)={
try {
resolve(fn(db))
} catch(err) {
reject(err)
}
})
})
}
通过上面的改造,就可以使用.then或async await 来获取任务结果了
commit(add).then(result=> console.log(result))
const result = await commit(add)
再来熟悉下indexDB原生的操作,并结合这些操作的特点,进一步封装commit
indexDB原生操作
先来看下indexDb原生的增删改查要如何实现,这里以新增为例
新增数据
对任何数据表(对象库)的操作,请务必通过事务来完成,才不至于出现当有某一个步骤失败时,只修改了部分数据的情况,事务会自动将已改动的数据复原
const request = window.indexedDB.open('db1')
request.onsuccess = (e)=>{
const db = request.result
// 创建事务
const transcation = db.transcation('person', 'readwrite')
// 确定要操作的表
const persons = transaction.objectStore('person');
// 创建数据
const person = {
id: 5,
name: 'xiaozhang',
age: 0
}
// 添加数据到person表(对象库)
const request = persons.add(person)
// 获取操作的结果,成功,或失败
request.onsuccess = function() {
console.log("add success", request.result)
}
request.onerror = function(err) {
console.log("add error", err)
}
}
request.onupgradeneeded = function (event) {
// 初始化一个person表, 指定id为主键
const store = db.createObjectStore('person', { keyPath: 'id' })
};
游标
当需要批量查询数据时,为了避免数据量过大而出现性能问题,我们需要使用游标一条一条获取数据
const db = request.result
// 创建事务
const transcation = db.transcation('person', 'readwrite')
// 确定要操作的表
const persons = transaction.objectStore('person');
// 现在假设person表有很多学生,并且每个学生都有头像,以二进制格式存储在数据库中
const request = persons.onenCursor()
const res = []
request.onsuccess = (e)=>{
const cursor = e.target.result
if(cursor) {
res.push(cursor.value)
console.log(cursor.value)
cursor.continue()
}
}
可以发现,事务提交有以下几个固定步骤
-
每一次commit都应该提交一次事务
-
监听每次一事务的操作
- onsuccess对应Promise resolve,
- onerror 对应 reject
与此同时,事务也存在一些会随着操作需要而变化的地方
- 表名(对象库名)
- 操作类型 readwrite | readonly
const transcation = db.transcation('person', 'readwrite')
- 需执行的具体操作
// 创建数据
const person = {
id: 5,
name: 'xiaozhang',
age: 0
}
// 添加数据到person表(对象库)
const request = persons.add(person)
所以,固定步骤就我们可以直接硬编码在commit函数中,而变化的地方,则需要外部传入。知道了这些,就可以对commit进行下一步改造了
针对简单事务的封装
我们结合indexDB事务的特点,再改造一次commit方法
// 传入 tableName
// 事务 commit
// 操作类型 mode
// 具体操作 callback
const commit = (db, tableName, commit, mode = 'readwrite', callback)=> {
return new Promise((resolve, reject)=>{
const task = (db)=>{
try {
if(!db) {
let store = db.transaction(tablename, mode).objectStore(tablename)
// 提交一个自定义事务
if(!commit) {
callback && callback(null, resolve, store)
}
let res = commit(store)
res.onsuccess = (e)=>{
if(callback) {
callback(e, resolve, store)
} else {
resolve(e)
}
}
res.onerror = (err) => {
reject(err)
}
} else {
throw Error('数据库未开启')
}
} catch (error) {
reject(error)
}
}
if(!db) {
queue.push(task)
} else {
task()
}
})
}
上面的封装只能针对单次的事务,对于游标,我们需要手动调用continue()方法来获取下一个数值,为了避免重复代码,可以封装一个方法来帮助我们自动调用continue
// e indexDb的事件对象
// condition 外部传入的过滤器 返回 boolean类型
// handler 让外部能够获取到当前遍历的值
// success 遍历完所有数据时调用
const cursorSuccess = (e, { condition, handler, success}) {
const cursor = e.target.result
if(cursor) {
const currentValue = cursor.value
if(condition(currentValue)) {
handler({cursor, currentValue})
}
cursor.continue()
} else {
success()
}
}
使用我们封装的方法来操作数据库
以下是前面已经封装好的基础方法,放在这里方便查看
const queue = [] // 任务队列
const request = window.indexedDB.open('testDb1'); // 连接数据库
// 执行任务
const exec = (db)=>{
let fn = null
while(queue.length) {
fn = queue.pop()
try {
fn(db)
} catch(e) {
console.log(e)
}
}
}
// 数据库连接成功后执行任务
request.onsuccess = ()=>{
exec(db)
}
// 这些是前面定义好的方法
新增数据
新增条person数据
const person = {name: 'xieziqian', age: 999}
// db传null, 会将这次事务存储到queue中,等待数据库连接成功后执行
const status = await commit(null, 'person', transaction.add(person), 'readwrite', (e, resolve)=>{
resolve(e)
})
游标
利用commit + cursotSuccess遍历所有的person,
const personList = commit(null, 'person', transaction.openCursor(), (e, resolve)=> {
const res = []
cursorSuccess(e, {
condition: (person)=> person.age < 18,
handler({ currentValue, cursor }) {
res.push(currentValue)
},
success() {
resolve(res)
}
})
})
删除所有person
const personList = commit(null, 'person', transaction.openCursor(), (e, resolve)=> {
const res = []
cursorSuccess(e, {
condition: (person)=> person.age < 18,
handler({ currentValue, cursor }) {
res.push(currentValue)
currentValue.delete()
},
success() {
resolve(res)
}
})
})
EzIndexDB
在上面两个方法的基础上,可以封装一些常用的查询,并放到一个类中
以下是完整代码
class EzIndexDb {
dbName = ''
version = 1
tableList = []
db = null
queue = []
_instance = null
mapCondition = {
eq: IDBKeyRange.only,
gt: IDBKeyRange.lowerBound,
lt: IDBKeyRange.upperBound,
between: IDBKeyRange.bound
}
constructor({ dbName, version, tables }) {
this.dbName = dbName
this.version = version
this.tables = tables
}
_result(e) {
return e.target.result || null
}
_getTransaction(value, type) {
return
}
static getInstance(dbOptions) {
// 单例
if (EzIndexDb._instance) {
return EzIndexDb._instance
} else {
EzIndexDb._instance = new EzIndexDb(dbOptions)
}
return EzIndexDb._instance
}
// 提交事务
commit(tablename, commit, mode = 'readwrite', callback) {
return new Promise((reslove, reject) => {
const task = () => {
try {
if (this.db) {
let store = this.db.transaction(tablename, mode).objectStore(tablename)
if (!commit) {
// 自定义事务
callback(null, reslove, store)
return
}
// 监听事务的状态
let res = commit(store)
res.onsuccess = (e) => {
if (callback) {
callback(e, reslove, store)
} else {
reslove(e)
}
}
res.onerror = (err) => {
reject(err)
}
} else {
throw new Error('数据库未开启')
}
} catch (e) {
reject(e)
}
if (!this.db) {
this.queue.push(task)
} else {
task()
}
}
})
}
// 游标回调
cursorSuccess(e, { condition, handler, success }) {
const cursor = this._result(e)
if (cursor) {
const currentValue = cursor.value
if (condition(currentValue)) {
handler({ cursor, currentValue })
}
cursor.continue()
} else {
success()
}
}
// 创建一个表
createTable(idb, { tableName, option, indexs = [] }) {
if (!idb.objectStoreNames.contains(tableName)) {
let store = idb.createObjectStore(tableName, option)
for (let { key, option } of indexs) {
store.createIndex(key, key, option)
}
}
}
// 删除一个表
deleteTable(tableName) {
return this.commit(tableName, (transaction) => transaction.clear(), 'readwrite', (_, resolve) => resolve())
}
// 删库
deleteDb(name) {
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(name)
request.onsuccess = resolve
request.onerror = reject
request.onblocked = reject
})
}
// 关闭连接
closeDb() {
return new Promise((resolve, reject) => {
try {
if (!this.db) {
resolve('数据库未开启')
return
}
this.db.close();
this.db = null;
EzIndexDb._instance = null
resolve(true)
} catch (err) {
reject(err)
}
})
}
// 打开数据库
openDb() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version)
request.onsuccess = (event) => {
this.db = this._result(event)
let task = null
while (this.queue.length) {
task = this.queue.pop()
if (task) {
task()
} else {
reject(null)
throw new Error('事务执行失败!')
}
}
resolve(this)
}
})
}
// 删除主键
deleteByPrimaryKey({ tableName, value }) {
return this.commit(tableName, (transaction) => transaction.delete(value), 'readwrite', (_, resolve) => resolve())
}
// 使用游标删除每一个数据
delete({ tableName, condition }) {
const res = []
return this.commit(tableName, transaction => transaction.openCursor(), 'readwrite', (e, resolve) => {
this.cursorSuccess(e, {
condition,
handler({ currentValue, cursor }) {
res.push(currentValue)
currentValue.delete()
},
// 删除成功后返回已经删除的数据
success() {
resolve(res)
}
})
})
}
// 插入数据
insert({ tableName, data }) {
return this.commit(tableName, undefined, 'readwrite', (_, resolve, store) => {
Array.isArray(data) ? data.forEach(value => store.put(value)) : store.put(data)
resolve()
})
}
// 根据主键更新某条数据
updateByPrimaryKey({ tableName, value, handle }) {
return this.commit(tableName, transaction => transaction.get(value), 'readwrite', (e, resolve, store) => {
const currentValue = this._result(e)
if (!currentValue) {
resolve(null)
return
}
// 将当前数据的引用传递给用户
const value = handle(currentValue)
store.put(value)
resolve(value)
})
}
// 更新数据
update(tableName, condition, handle) {
const res = []
return this.commit(tableName, transaction => transaction.openCursor(), 'readwrite', (e, resolve, store) => {
this.cursorSuccess(e, {
condition,
handler({ currentValue, cursor }) {
const value = handle(currentValue)
res.push(value)
cursor.update(value)
},
success() {
resolve(res)
}
})
})
}
// 以主键查询表中某一条数据
queryByPrimaryKey({ tableName, value }) {
return this.commit(tableName, transaction => transaction.get(value), 'readonly', (e, resolve) => [
resolve(this._result(e))
])
}
// 以索引查询某一条数据
queryByKeyValue({ tableName, key, value }) {
return this.commit(tableName, transaction => transaction.index(key).get(value), 'readonly', (e, resolve) => {
resolve(this._result(e))
})
}
// 查询表中符合条件的所有数据
query({ tableName, condition }) {
const res = []
return this.commit(tableName, transaction => transaction.openCursor(), 'readonly', (e, resolve)=>{
this.cursorSuccess(e, {
condition,
handler: ({currentValue}) => {
res.push(currentValue)
},
success: ()=>{
resolve(res)
}
})
})
}
queryAll({ tableName }) {
const res = []
return commit(table, transaction => transaction.openCursor(), 'readonly', (e, resolve)=>{
this.cursorSuccess(e, {
handler: ({currentValue}) =>{
res.push(currentValue)
},
success: () => {
resolve(res)
}
})
})
}
count({ tableName, key, condition }) {
return this.commit(tableName, transaction => transaction.index(key).count(this.mapCondition[condition.type](...condition.rangeValue)), 'readonly', (e, resolve) => {
resolve(this._result(e))
})
}
}
const db = EzIndexDb.getInstance({
dbName: 'testEzDb',
version: 1,
tables: []
})
db.openDb()
const ezDb = EzIndexDb.getInstance()