ES6(四)用Promise封装一下IndexedDB(上)

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。

使用方法在下一篇介绍。

源码

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

谦行: https://www.cnblogs.com/dolphinX/p/3416889.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容