zoukankan      html  css  js  c++  java
  • vue3 专用 indexedDB 封装库,基于Promise告别回调地狱

    IndexedDB 的官网

    https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API
    这个大概是官网吧,原始是英文的,现在陆续是出中文版。有空的话还是多看看官网。

    简介

    IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 可以使用索引实现对数据的高性能搜索。

    简单的说就是 —— 能装!indexedDB 是前端的一种“事务型对象数据库”,可以存放很多很多的对象(当然也可以是其他类型),功能很强大,可以用作数据(对象)在前端的缓存容器。或者其他用途。

    使用也是很简单的,网上可以找到很多教程,官网也推荐了几个封装类库。
    只是我比较懒,得看别人的类库(好吧,我看不懂),而是想按照自己的想法封装一个自己用着习惯的类库。

    最近在使用 Vue3,所以想针对 Vue3 做一套 indexedDB 的类库,实现客户端缓存数据的功能。

    其实这里介绍的应该算是第二版了,第一版在项目里面试用一段时间后,发现了几个问题,所以想在新版本里面一起解决。

    indexedDB 的操作思路

    一开始看 indexedDB 的时候各种懵逼,只会看大神写的文章,然后照猫画虎,先不管原理,把代码copy过来能运行起来,能读写数据即可。

    现在用了一段时间,有了一点理解,整理如下:

     使用思路

    • 获取 indexedDB 的对象
    • open (打开/建立)数据库。
    • 如果没有数据库,或者版本升级:
      • 调用 onupgradeneeded(建立/修改数据库),然后调用 onsuccess。
    • 如果已有数据库,且版本不变,那么直接调用 onsuccess。
    • 在 onsuccess 里得到连接对象后:
      • 开启事务。
        • 得到对象仓库。
          • 执行各种操作:添加、修改、删除、获取等。
          • 用索引和游标实现查询。
        • 得到结果

    思路明确之后,我们就好封装了。

    做一个 help,封装初始化的代码

    前端数据库和后端数据库对比一下,就会发现一个很明显的区别,后端数据库是先配置好数据库,建立需要的表,然后添加初始数据,最后才开始运行项目。

    在项目里面不用考虑数据库是否已经建立好了,直接用就行。

    但是前端数据库就不行了,必须先考虑数据库有没有建立好,初始数据有没有添加进去,然后才可以开始常规的操作。

    所以第一步就是要封装一下初始化数据库的部分。

    我们先建立一个 help.js 文件,在里面写一个 ES6 的 class。

    
    /**
     * indexedDB 的 help,基础功能的封装
     * * 打开数据库,建立对象仓库,获取连接对象,实现增删改查
     * * info 的结构:
     * * * dbFlag: '' // 数据库标识,区别不同的数据库
     * * * dbConfig: { // 连接数据库
     * * * * dbName: '数据库名称',
     * * * * ver: '数据库版本',
     * * * },
     * * * stores: {
     * * * * storeName: { // 对象仓库名称
     * * * * * id: 'id', // 主键名称
     * * * * * index: { // 可以不设置索引
     * * * * * * name: ture, // key:索引名称;value:是否可以重复
     * * * * * }
     * * * * }
     * * * },
     * * * init: (help) => {} // 完全准备好之后的回调函数
     */
    export default class IndexedDBHelp {
      constructor (info) {
        this.myIndexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB
        if (!this.myIndexedDB) {
          console.log('您的浏览器不支持IndexedDB')
        }
        // 数据库名称和版本号
        this._info = {
          dbName: info.dbConfig.dbName,
          ver: info.dbConfig.ver
        }
        // 记录连接数据库的对象, IDBDatabase 类型,因为open是异步操作,所以不能立即获得。
        this._db = null
    
        // 记录仓库状态。new:新库或者版本升级后;old:有对象仓库了。
        this._storeState = 'pending'
    
        /**
         * 注册回调事件。
         * * 如果组件读写 indexedDB 的时还没有准备好的话,
         * * 可以来注册一个事件,等准备好了之后回调。
         */
        this._regCallback = []
    
        // 打开数据库,异步操作,大概需要几毫秒的时间。
        this.dbRequest = this.myIndexedDB.open(this._info.dbName, this._info.ver)
    
        // 第一次,或者版本升级时执行,根据配置信息建立表
        this.dbRequest.onupgradeneeded = (event) => {
          this._storeState = 'new'
          const db = event.target.result
          // console.log('【2】新建或者升级数据库 onupgradeneeded --- ', db)
    
          for (const key in info.stores) {
            const store = info.stores[key]
            if (db.objectStoreNames.contains(key)) {
              // 已经有仓库,验证一下是否需要删除原来的仓库
              if (store.isClear) {
                // 删除原对象仓库,没有保存数据
                db.deleteObjectStore(key)
                // 建立新对象仓库
                const objectStore = db.createObjectStore(key, { keyPath: store.id })
                // 建立索引
                for (const key2 in store.index) {
                  const unique = store.index[key2]
                  objectStore.createIndex(key2, key2, { unique: unique })
                }
              }
            } else {
              // 没有对象仓库,建立
              const objectStore = db.createObjectStore(key, { keyPath: store.id }) /* 自动创建主键 autoIncrement: true */
              // 建立索引
              for (const key2 in store.index) {
                const unique = store.index[key2]
                objectStore.createIndex(key2, key2, { unique: unique })
              }
            }
          }
        }
    
        // 数据库打开成功,记录连接对象
        this.dbRequest.onsuccess = (event) => {
          this._db = event.target.result // dbRequest.result
          // console.log('【1】成功打开数据库 onsuccess --- ', this._db)
          // 修改状态
          if (this._storeState === 'pending') {
            this._storeState = 'old'
          }
          // 调用初始化的回调
          if (typeof info.init === 'function') {
            info.init(this)
          }
          // 调用组件注册的回调
          this._regCallback.forEach(fn => {
            if (typeof fn === 'function') {
              fn()
            }
          })
        }
    
        // 处理出错信息
        this.dbRequest.onerror = (event) => {
          // 出错
          console.log('打开数据库出错:', event.target.error)
        }
      }
      // 挂载其他操作,后面介绍。。。
    }
    

    这里要做几个主要的事情:

    • 判断浏览器是否支持 indexedDB
    • 打开数据库
    • 设置对象仓库
    • 保存连接对象,备用

    另外使用 jsDoc 进行参数说明,有的时候是可以出现提示,就算不出提示,也是可以有说明的作用,避免过几天自己都想不起来怎么用参数了。

    参数提示

    挂载事务

    拿到数据库的连接对象之后,我们可以(必须)开启一个事务,然后才能执行其他操作。

    所以我们需要先把事务封装一下,那么为啥要单独封装事务呢?

    因为这样可以实现打开一个事务,然后传递事务实例,从而实现连续操作的目的,虽然这种的情况不是太多,但是感觉还是应该支持一下这种功能。

    begin-tran.js

    /**
     * 开启一个读写的事务
     * @param {*} help indexedDB 的 help
     * @param {Array} storeName 字符串的数组,对象仓库的名称
     * @param {string} type readwrite:读写事务;readonly:只读事务;versionchange:允许执行任何操作,包括删除和创建对象存储和索引。
     * @returns 读写事务
     */
    const beginTran = (help, storeName, type = 'readwrite') => {
      return new Promise((resolve, reject) => {
        const _tran = () => {
          const tranRequest = help._db.transaction(storeName, type)
          tranRequest.onerror = (event) => {
            console.log(type + ' 事务出错:', event.target.error)
            reject(`${type} 事务出错:${event.target.error}`)
    
          }
          resolve(tranRequest)
          tranRequest.oncomplete = (event) => {
            // console.log('beginReadonly 事务完毕:', window.performance.now())
          }
        }
    
        if (help._db) {
          _tran() // 执行事务
        } else {
          // 注册一个回调事件
          help._regCallback.push(() => _tran())
        }
      })
    }
    export default beginTran
    
    • 支持多个对象仓库
      storeName 是字符串数组,所以可以针对多个对象仓库同时开启事务,然后通过 tranRequest.objectStore(storeName) 来获取具体的对象仓库。

    • 挂载到 help
      因为是写在单独的js文件里面,所以还需要在 help 里面引入这个js文件,然后挂到 help 上面,以便实现 help.xxx 的调用形式,这样拿到help即可,不用 import
      各种函数了。

    import _beginTran from './begin-tran.js' // 事务
    ... help 的其他代码
      // 读写的事务
      beginWrite (storeName) {
        return _beginTran(this, storeName, 'readwrite')
      }
    
      // 只读的事务
      beginReadonly (storeName) {
        return _beginTran(this, storeName, 'readonly')
      }
    

    是不是有一种“循环调用”的感觉?js 就是可以这么放飞自我。然后需要我们写代码的时候就要万分小心,因为不小心的话很容易写出来死循环。

    挂载增删改查

    事务准备好了,我们就可以进行下一步操作。

    先设计一个添加对象的 js文件:

    addModel.js

    import _vueToObject from './_toObject.js'
    
    /**
     * 添加对象
     * @param { IndexedDBHelp } help 访问数据库的实例
     * @param { string } storeName 仓库名称(表名)
     * @param { Object } model 对象
     * @param { IDBTransaction } tranRequest 如果使用事务的话,需要传递开启事务时创建的连接对象
     * @returns 新对象的ID
     */
    export default function addModel (help, storeName, model, tranRequest = null) {
      // 取对象的原型,便于保存 reactive 
      const _model = _vueToObject(model)
      // 定义一个 Promise 的实例
      return new Promise((resolve, reject) => {
        // 定义个函数,便于调用
        const _add = (__tran) => {
          __tran
            .objectStore(storeName) // 获取store
            .add(_model) // 添加对象
            .onsuccess = (event) => { // 成功后的回调
              resolve(event.target.result) // 返回对象的ID
            }
        }
        if (tranRequest === null) {
          help.beginWrite([storeName]).then((tran) => {
            // 自己开一个事务
            _add(tran)
          })
        } else {
          // 使用传递过来的事务
          _add(tranRequest)
        }
      })
    }
    
    

    首先使用 Promise 封装默认的回调模式,然后可以传递进来一个事务进来,这样可以实现打开事务连续添加的功能。

    如果不传递事务的话,内部会自己开启一个事务,这样添加单个对象的时候也会很方便。

    然后在 help 里面引入这个 js文件,再设置一个函数:

    import _addModel from './model-add.js' // 添加一个对象
    
      /**
       * 添加一个对象
       * @param {string} storeName 仓库名称
       * @param {object} model 要添加的对象
       * @param {*} tranRequest 事务,可以为null
       * @returns 
       */
      addModel (storeName, model, tranRequest = null) {
        return _addModel(this, storeName, model, tranRequest = null)
      }
    

    这样就可以挂载上来了。把代码分在多个 js文件里面,便于维护和扩展。

    使用提示

    修改、删除和获取的代码也类似,就不一一列举了。

    使用方式

    看了上面的代码可能会感觉很晕,这么复杂?不是说很简单吗?

    对呀,把复杂封装进去了,剩下的就是简单的调用了。那么如何使用呢?

    准备创建数据库的信息

    我们先定义一个对象,存放需要的各种信息

    const dbInfo = {
      dbFlag: 'project-meta-db', // 数据库标识,区分不同的数据库。如果项目里只有一个,那么不需要加这个标识
      dbConfig: {
        dbName: 'nf-project-meta', // 数据库名称
        ver: 2
      },
      stores: { // 数据库里的表(对象仓库)
        moduleMeta: { // 模块的meta {按钮,列表,分页,查询,表单若干}
          id: 'moduleId',
          index: {},
          isClear: false
        },
        menuMeta: { // 菜单用的meta
          id: 'id',
          index: {},
          isClear: false
        },
        serviceMeta: { // 后端API的meta,在线演示用。
          id: 'moduleId',
          index: {},
          isClear: false
        }
      },
      init: (help) => {
        // 数据库建立好了
        console.log('inti事件触发:indexedDB 建立完成 ---- help:', help)
      }
    }
    
    • dbFlag
      一个项目里面可能需要同时使用多个 indexedDB 的数据库,那么就需要一个标识区分一下,dbFlag 就是区分标识。

    • stores
      对象仓库的说明,在 onupgradeneeded 事件里面依据这个信息创建对象仓库。

    • init
      indexedDB 都准备好之后的回调函数。

    直接使用

    import IndexedDB from '../../../packages/nf-ws-indexeddb/help.js'
    
    // 建立实例
    const help = new IndexedDB(dbInfo)
    
    // 添加对象的测试
    const add = () => {
      // 定义一个对象
      const model = {
        id: new Date().valueOf(),
        name: 'test'
      }
      // 添加
      help.addModel('menuMeta', model).then((res) => {
        console.log('添加成功!', res) // 返回对象ID
      })
    }
    
    • 定义一个数据库描述信息
    • 生成 help 的实例
    • 使用 help.addModel 添加对象

    做个“外壳”套个娃

    检查一下代码,发现有几个小问题:

    • 每次使用都需要实例化一个help吗?是不是有点浪费?
    • 对象仓库名还需要写字符串,万一写错了怎么办?
    • help.xxxModel(xxx,xxx,xxx) 是不是有点麻烦?

    所以我们需要在套一个外壳,让使用更方便。

    
    import IndexedDB from './help.js'
    
    /**
     * 把 indexedDB 的help 做成插件的形式
     */
    export default {
      _indexedDBFlag: Symbol('nf-indexedDB-help'),
      _help: {}, // 访问数据库的实例
      _store: {}, // 存放对象,实现 foo.addModel(obj)的功能 
      
      createHelp (info) {
        let indexedDBFlag = this._indexedDBFlag
        if (typeof info.dbFlag === 'string') {
          indexedDBFlag = Symbol.for(info.dbFlag)
        } else if (typeof info.dbFlag === 'symbol') {
          indexedDBFlag = info.dbFlag
        }
        // 连接数据库,获得实例。
        const help = new IndexedDB(info)
        // 存入静态对象,以便于支持保存多个不同的实例。
        this._help[indexedDBFlag] = help // help
        this._store[indexedDBFlag] = {} // 仓库变对象
    
        // 把仓库变成对象的形式,避免写字符串的仓库名称
        for (const key in info.stores) {
          this._store[indexedDBFlag][key] = {
            put: (obj) => {
              let _id = obj
              if (typeof obj === 'object') {
                _id = obj[info.stores[key].id]
              }
              return help.updateModel(key, obj, _id)
            },
            del: (obj) => {
              let _id = obj
              if (typeof obj === 'object') {
                _id = obj[info.stores[key].id]
              }
              return help.deleteModel(key, _id)
            },
            add: (obj) => help.addModel(key, obj),
            get: (id = null) => help.getModel(key, id)
          }
        }
      },
    
      // 获取静态对象里的数据库实例
      useDBHelp (_dbFlag) {
        let flag = this._indexedDBFlag
        if (typeof _dbFlag === 'string') {
          flag = Symbol.for(_dbFlag)
        } else if (typeof _dbFlag === 'symbol') {
          flag = _dbFlag
        }
        return this._help[flag]
      },
      useStore (_dbFlag) {
        let flag = this._indexedDBFlag
        if (typeof _dbFlag === 'string') {
          flag = Symbol.for(_dbFlag)
        } else if (typeof _dbFlag === 'symbol') {
          flag = _dbFlag
        }
        return this._store[flag]
      }
    }
    
    

    首先,这是一个静态对象,可以存放 help 的实例,可以实现全局访问的效果。

    以前是 使用 provide / inject 保存的,但是发现有点不太方便,也不是十分必要,所以改成了静态对象的方式。

    然后根据建表的信息,创建仓库的对象,把字符串的仓库名称变成对象的形式,这样就方便多了。

    为啥是 “useDBHelp”呢,因为要和 webSQL的 help 加以区分。

    使用的时候就变成了这样:

    
    // 把仓库当做“对象”
    const  { menuMeta }  = dbInstall.useStore(dbInfo.dbFlag)
    
    // 添加对象
    const add = () => {
      const t1 = window.performance.now()
      console.log('
     -- 准备添加对象 --:', t1)
      const model = {
        id: new Date().valueOf(),
        name: 'test-。'
      }
      menuMeta.add(model).then((res) => {
        const t2 = window.performance.now()
        console.log('添加成功!', res, '用时:', t2 - t1, '
    ')
      })
    }
    

    这样的话,就方便多了。对象仓库名.xxx(oo) 就可以,代码简洁了很多。

    进一步套娃

    上面是把对象仓库看做了“对象”,然后实现增删改查,那么能不能让object 本身实现增删改查呢?

    既然封装到这一步了,我们可以再前进一下,使用 js的原型 实现 object 的增删改查。

    // 给 model 加上增删改查的函数
    for (const key in info.stores) {
      this._store[indexedDBFlag][key] = {
        createModel: (model) => {
          function MyModel (_model) {
            for (const key in _model) {
              this[key] = _model[key]
            }
          }
          MyModel.prototype.add = function (tran = null) {
            return help.addModel(key, this, tran)
          }
          MyModel.prototype.save = function (tran = null) {
            const _id = this[info.stores[key].id]
            return help.setModel(key, this, _id, tran)
          }
          MyModel.prototype.load = function (tran = null) {
            return new Promise((resolve, reject) => {
              // 套个娃
              const _id = this[info.stores[key].id]
              help.getModel(key, _id, tran).then((res) => {
                Object.assign(this, res)
                resolve(res)
              })
            })
          }
          MyModel.prototype.del = function (tran = null) {
            const _id = this[info.stores[key].id]
            return help.delModel(key, _id, tran)
          }
          const re = new MyModel(model)
          return reactive(re)
        }
      }
    }
    

    首先给对象仓库加一个 “createModel”函数,用于把 object 和对象仓库挂钩,然后用原型挂上增删改查的函数,最后 new 一个实例返回。

    使用方式:

    
    // 对象仓库,创建一个实例,reactive 形式
    const testModel = menuMeta.createModel({
      id: 12345,
      name: '对象自己save'
    })
     
    // 对象直接保存
    const mSave = () => {
      testModel.name = '对象自己save' + window.performance.now()
      testModel.save().then((res) => {
        // 保存完成
      })
    }
    

    因为加上了 reactive,所以可以自带响应性。
    这样是不是很像“充血实体类”了?

    id 值建议不要修改,虽然可以改,但是总感觉改了的话比较别扭。

    统一“出口”

    虽然用 help 带上了几个常规操作,但是出口还是不够统一,像 Vue 那样,就一个出口是不是很方便呢?所以我们也要统一一下:

    storage.js

    // 引入各种函数,便于做成npm包
    // indexedDB 部分
    import dbHelp from './nf-ws-indexeddb/help.js'
    import dbInstall from './nf-ws-indexeddb/install.js'
    
    // indexedDB 部分
    const dbCreateHelp = (info) => dbInstall.createHelp(info)
    const useDBHelp = (_dbFlag) => dbInstall.useDBHelp(_dbFlag)
    const useStores = (_dbFlag) => dbInstall.useStores(_dbFlag)
    
    export {
      // indexedDB 部分
      dbHelp, // indexedDB 的 help
      dbCreateHelp, // 创建 help 实例,初始化设置
      useDBHelp, // 组件里获取 help 的实例
      useStores // 组件里获取对象仓库,方便实现增删改查
    }
    

    这样也便于我们打包发布到npm。

    在 vue 里面使用

    基本工作都作好了,就剩最后一个问题了,在 Vue3 里面如何使用呢?

    我们可以仿造一下 vuex 的使用方式,先建立一个 js文件,实现统一设置。

    store-project/db.js

    // 引入 indexedDB 的 help
    import { dbCreateHelp } from '../../packages/storage.js'
    
    // 引入数据库数据
    const db = {
      dbName: 'nf-project-meta',
      ver: 5
    }
    
    /**
     * 设置
     */
    export default function setup (callback) {
      const install = dbCreateHelp({
        // dbFlag: 'project-meta-db',
        dbConfig: db,
        stores: { // 数据库里的表
          moduleMeta: { // 模块的meta {按钮,列表,分页,查询,表单若干}
            id: 'moduleId',
            index: {},
            isClear: false
          },
          menuMeta: { // 菜单用的meta
            id: 'id',
            index: {},
            isClear: false
          },
          serviceMeta: { // 后端API的meta,在线演示用。
            id: 'moduleId',
            index: {},
            isClear: false
          },
          testIndex: { // 测试索引和查询。
            id: 'moduleId',
            index: {
              kind: false,
              type: false
            },
            isClear: false
          }
        },
        // 加入初始数据
        init (help) {
          if (typeof callback === 'function') {
            callback(help)
          }
        }
      })
      return install
    }
    
    

    然后在 main.js 里面调用,因为这是最早执行代码的地方,可以第一时间建立数据库。

    
    // 引入 indexedDB 的help
    import dbHelp from './store-project/db.js'
    
    dbHelp((help) => {
      // indexedDB 准备好了
      console.log('main里面获取 indexedDB 的help', help)
    })
    

    同时可以把 help 的实例存入静态对象里面。

    其实一开始是使用 provide 注入的,但是发现不是太适合,因为在main.js这个层级里面无法使用inject读取出来,这样的话,和状态等的操作就不太方便。

    所以干脆放在静态对象里面好了,任何地方都可以访问到。

    并不需要使用 use 挂载到 App 上面。

    索引和查询

    由于篇幅有限,这里就先不介绍了,如果大家感兴趣的话,可以在写一篇补充一下。

    源码

    封装前端存储
    https://gitee.com/naturefw/nf-web-storage

    在线演示

    https://naturefw.gitee.io/vite2-vue3-demo

    安装方式

    yarn add nf-web-storage
    
  • 相关阅读:
    【纯水题】POJ 1852 Ants
    【树形DP】BZOJ 1131 Sta
    【不知道怎么分类】HDU
    【树形DP】CF 1293E Xenon's Attack on the Gangs
    【贪心算法】CF Emergency Evacuation
    【思维】UVA 11300 Spreading the Wealth
    【树形DP】NOI2003 逃学的小孩
    【树形DP】BZOJ 3829 Farmcraft
    【树形DP】JSOI BZOJ4472 salesman
    【迷宫问题】CodeForces 1292A A NEKO's Maze Game
  • 原文地址:https://www.cnblogs.com/jyk/p/15406649.html
Copyright © 2011-2022 走看看