手把手教你写一个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()
全部评论

相关推荐

字节 飞书绩效团队 (n+2) * 15 + 1k * 12 + 1w
点赞 评论 收藏
分享
10-27 17:26
东北大学 Java
点赞 评论 收藏
分享
听说改名字就能收到offer哈:Radis写错了兄弟
点赞 评论 收藏
分享
头像
昨天 15:46
已编辑
中南大学 后端
字节国际 电商后端 24k-35k
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务