IndexedDB工具函数

泄露秘密 提交于 2019-12-15 01:19:20
/*
 * @author visy.wang
 * @date 2019/12/9 17:17
 */
//IDB.js
export default {

  //创建一个数据库连接 (如果指定的数据库不存在则会新建一个数据库)
  //dbName:string 数据库名称,必填
  //version:number<int> 版本号,在没有第三个参数时可省略
  //options:array用于创建objectStore,但只有数据库新建和版本变更的时候才会被调用,可省略
  //它的格式如下:
  /*[{
     name: string,//store名称,必填
     options: object, //store配置选项,选填
     index: {//设置索引,选填,目前仅支持配置一个索引
       name: string, //索引名称,自定义(必填)
       fields: string/array<string>, //索引关联字段名,有多个时必须使用数组
       options: object //索引的配置选项,选填
     }
    },{
     //...
    },...]
   */
  open(dbName, version, options=[]){
    return new Promise((resolve, reject)=>{
      const indexedDB = window.indexedDB || window.mozIndexedDB ||
            window.webkitIndexedDB || window.msIndexedDB

      if(indexedDB){
        //打开一个数据库(或者新建)
        const request = indexedDB.open(dbName, version)

        //数据库连接出错回调
        request.onerror = ()=>{
          reject(this.error("数据库打开报错",{ dbName, version }))
        }

        //数据库连接成功回调
        request.onsuccess = evt=>{//数据操作
          const instance = this.newInstance()//创建一个IDB实例
          instance.request = request //保留request的引用
          const db = (instance.db = evt.target.result)//保留db的引用
          console.log("数据库打开成功:", db)
          resolve(instance)//返回IDB实例
        }

        //数据库升级成功回调
        request.onupgradeneeded = evt=>{//数据结构定义
          const db = evt.target.result //拿到最新的db对象
          console.log("数据库升级成功:", db)
          //根据配置项批量创建objectStore
          for(const option of options){
            const {name:storeName, options:storeOptions, index} = option
            if(!db.objectStoreNames.contains(storeName)){
              const objStore = db.createObjectStore(storeName, storeOptions)
              if(objStore && index){//创建索引
                const {name:indexName, fields, options:indexOptions } = index
                objStore.createIndex(indexName, fields, indexOptions)
              }
            }else{
              console.error("创建失败["+storeName+"已存在]")
            }
          }
        }
      }else{
        reject(this.error("当前环境不支持IndexedDB"))
      }
    })
  },

  //创建一个新的IDB实例(私有)
  newInstance(){
    const _this = this
    return {
      request: null,
      db: null,
      storeName: "",

      //指定objectStore
      useStore(storeName){
        this.storeName = storeName
        return this
      },

      //插入数据: insert(object): promise
      insert(row){
        return new Promise((resolve, reject)=>{
          const insert = _this.getObjStore(this, true).add(row)
          insert.onsuccess = function(){
            const pk = insert.result//insert.result就是主键
            _this.setPK(row, pk)
            resolve(pk)
          }
          insert.onerror = function(){
            reject(_this.error("添加记录出错",row))
          }
        })
      },

      //通过主键获取数据: get(any): promise
      get(pk){
        return new Promise((resolve, reject)=>{
          const get = _this.getObjStore(this).get(pk)
          //const index = objStore.index('name'); //使用索引
          //get = index.get('张三');
          get.onsuccess = function(){
            resolve(_this.setPK(get.result,pk))
          }
          get.onerror = function(){
            reject(_this.error("单行查询出错",pk))
          }
        })
      },

      //全量查询: select(): promise
      select(){
        return new Promise((resolve, reject)=>{
          const rows = [], select = _this.getObjStore(this).openCursor()
          select.onsuccess = function(evt){
            const cursor = evt.target.result
            if(cursor){
              rows.push(_this.setPK(cursor.value, cursor.key))
              cursor.continue()
            }else{
              resolve(rows)
            }
          }
          select.onerror = function(evt){
            reject(_this.error("全量查询出错",evt))
          }
        })
      },

      //更新记录: update(object): promise
      update(row){
        //.put()这玩意有点危险的地方是找不到要更新的记录,就会当作新增处理
        //.put()的第二个参数只有在创建objectStore时没有指定主键(keyPath)才需要传,否则会报错
        return new Promise((resolve, reject)=>{
          const objStore = _this.getObjStore(this, true)
          const update = objStore.put(row, objStore.keyPath? undefined: row._pk_)
          update.onsuccess = function(){
            resolve(true)
          }
          update.onerror = function(){
            reject(_this.error("更新记录出错",row))
          }
        })
      },

      //删除记录: delete(any): promise
      delete(pk){
        return new Promise((resolve, reject)=>{
          const del = _this.getObjStore(this, true).delete(pk)
          del.onsuccess = function(evt){
            resolve(true)
          }
          del.onerror = function(){
            reject(_this.error("删除记录出错",pk))
          }
        })
      },

      //批量添加: insertBatch(array<object>): promise
      insertBatch(rows=[]){
        const arr = [], objStore = _this.getObjStore(this, true)
        for(let item of rows){
          arr.push(((row,objStore)=>{//IIFE
            return new Promise((resolve, reject)=>{
              const insert = objStore.add(row)
              insert.onsuccess = function(){
                const pk = insert.result
                _this.setPK(row, pk)
                resolve(pk)
              }
              insert.onerror = function(){
                reject(_this.error("添加记录出错",row))
              }
            })
          })(item,objStore))
        }

        return Promise.all(arr).then(ids=>{
          return Promise.resolve(ids)
        }).catch(err=>{
          objStore.transaction.abort()//回滚
          throw err
        })
      },

      //关闭数据库
      close(){
        this.db && this.db.close()
      }
    }

  },

  //获取objectStore(私有)
  getObjStore({db, storeName}, writeable=false){
    if(storeName && db.objectStoreNames.contains(storeName)){
      //transaction的第二个参数默认值为"readonly"
      return db.transaction([storeName], writeable?"readwrite":"readonly").objectStore(storeName)
    }else{
      console.error("storeName未指定(调用useStore)或者指定的objStore不存在["+storeName+"]")
      return null
    }
  },

  //设置主键(私有)
  setPK(obj,val){
    obj!=null && Object.defineProperty(obj,"_pk_",{
      value: val,
      writable: false,
      enumerable: false,
      configurable: false
    })
    return obj
  },

  //error包装(私有)
  error(msg, detail){
    return {msg, detail}
  }
}

  • 使用IDB工具函数:
import IDB from "./IDB.js"

async useIDB(){
   //连接数据库,若指定数据库不存在时则新建一个数据库
   //每次open后获得的对象是新的
   //第一个参数是数据库名称
   //第二个参数的版本号,在没有第3个参数时可省略,当要修改数据库结构时请修改版本号(+1)
   //第三个参数为数组,可省略,可以使用它创建多个objectStore (新建数据库或者升级版本号时才生效)
   const idb = await IDB.open("myDb", 1, [{
     name: "user",//objectStore名称, 建议必填
     options: { keyPath: "userId" }//objectStore的配置选项,选填
   },{
     name: "person",
     options: { autoIncrement: true },
     index: {//设置索引,选填,目前仅支持配置一个索引
       name: "index_name",//索引名称,自定义(必填)
       fields: "name",//索引关联字段名,有多个时必须使用数组(至少填一个)
       options: { unique: true}//索引的配置选项,选填
     }
   }])

   //选定要操作的objectStore,必要的步骤
   idb.useStore("person")

   const ids = await idb.insertBatch([
     { name: "张三", age: 24, email: "zhangsan@qq.com" },
     { name: "李四", age: 22, email: "lisi@qq.com" },
     { name: "王五", age: 18, email: "wangwu@qq.com" }
   ])
   console.log("添加成功:", ids)

   const person = await idb.get(1)
   console.log("person=",person)
   person.age++
   const r = await idb.update(person)
   console.log("person 更新结果:", r)

   const personList = await idb.select()
   console.log("personList=", personList)

   idb.close()//关闭连接

   //获取一个新的数据库连接
   //这里可以继续使用idb(在idb没有关闭连接的情况下),
   //只需重新指定objectStore,像这样: idb.useStore("user")
   const idb2 = (await IDB.open("myDb",1)).useStore("user")

   console.log(idb === idb2)//可以看到二者不相同

   const userIds = await idb2.insertBatch([
     { userId:1, userName: "Jack", password: "111" },
     { userId:2, userName: "Victor", password: "333" },
     { userId:"33", userName: "Scott", password: "666" }
   ])
   console.log("user添加成功:", userIds)

   const user = await idb2.get("33")
   console.log("user=",user)
   user.password += "1"
   const rr = await idb2.update(user)
   console.log("user 更新结果:", rr)

   await idb2.delete(2)//删除

   console.log("userList=", await idb2.select())//查询全部
   
   //单个添加
   await idb2.insert({ userId:2, userName: "Victor", password: "333" })
   
   console.log("userList2=", await idb2.select())//查询全部

   //用完后关闭数据库连接
   idb2.close()
}

useIDB()//调用

IDB中的函数仅open()是公开的,请不要使用其他函数(因为它们是设计给内部使用的,如果在外部使用会有意想不到的结果,总之,不要使用它们)

IDB.open以及IDB实例(即通过IDB.open()异步获得的结果,且叫它idb)中关于数据库操作的所有函数(除了useStore()和close())都是异步的,请结合async/await或者promise相关规范使用它们

在数据库相关操作结束后(由于数据库相关操作是异步的,所以判断结束时要小心)调用idb.close()关闭数据库是个好习惯,也是很有必要的

关于idb.update(row):
这玩意有点危险的地方是找不到要更新的记录,就会当作新增处理,应该特别注意这一点,务必确保row包含有主键(即含有keyPath指定的字段,或者没有指定keyPath时含有"_pk_"字段)

关于"_pk_“的特别说明:
idb中针对数据库的相关操作会自动向操作的记录对象中添加一个不可修改的属性 “_pk_”,这是由于有些objectStore在创建的时候可能不会指定keyPath,而是采用自动生成的自增主键,这可能导致我们无法去标识数据,加上keyPath指定的属性并不总是可信任的(因为你可以修改它们),所以 idb会将主键写入到”_pk_"属性,并且它是不可修改的, "_pk_“的引入是为了方便在数据库操作时获取到可信任的主键,下面是相关操作与”_pk_"的详细说明:

  • insert(row) 添加成功后会将主键写入row
  • insertBatch([item1,item2,…])添加成功后会将每个item对应的主键写入item中
  • get(pk)会将主键写入返回结果中
  • select()会将每个item的主键写入返回结果中
  • update(row)的row推荐使用get/select得到的数据记录来进行此操作(以便idb自动获取主键),如果你使用自己创建的row,并且对应的objectStore没有指定keyPath,需要通过row._pk_ 手动添加主键,否则会造成新增(而不是更新)数据
  • delete(pk)的pk推荐使用get/select得到的数据记录,通过"row._pk_"获取主键
  • 通过前面4种方式添加过主键的对象叫做 “数据库原生对象”(insert的row,insertBatch的所有item,get返回结果,select返回结果中的所有item),对数据的修改应该尽量在“数据库原生对象”上操作
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!