IndexedDB的正确打开方式

2,447 阅读5分钟

这里不会介绍基本概念,请前往MDN阮一峰老师的IndexedDB自行学习

version的正确用法

关于open

var request = window.indexedDB.open(name, version);

第一个参数很简单,就是name。

第二个参数version是有说法的。version决定了数据库的结构,比如有哪些store,每个store是什么样的。

The version of the database determines the database schema — the object stores in the database and their structure.

  • 如果数据库不存在,立即就创建数据库,同时触发onupgradeneeded事件
  • 如果数据库存在
    • version不变,不会触发onupgradeneeded事件
    • version升级了,会触发onupgradeneeded事件

错误的结论

根据这些信息,很容易让我得出了❎错误的结论:

  • version:1,触发onupgradeneeded事件,在事件中创建名为member的store;
  • version:2,触发onupgradeneeded事件,在事件中创建名为spend的store;
  • 依次类推,等等。

根据实践发现,这个结论在特定条件下才能成功执行,所以是个错误的结论,也就是说是个错误的做法。

   // 错误案例 执行下来会发现createSpendStore()永远得不到执行
   var req1 = window.indexedDB.open(name, 1)
   req1.onupgradeneeded = function(){createMemberStore()}
   var req2 = window.indexedDB.open(name, 2)
   req2.onupgradeneeded = function(){createSpendStroe()}

原因就是open方法不会立即打开数据库或开始执行事务,而是返回一个event,如果我们顺序执行两次open,其实最终得到的是version:2的event,所以只会触发一次onupgradeneeded事件,也就是req1.onupgradeneeded得到了执行。

The open request doesn't open the database or start the transaction right away. The call to the open() function returns an IDBOpenDBRequest object with a result (success) or error value that you handle as an event.

至于特定条件下才能成功指的是,如果我们等req1全部执行完毕后,再执行req2的话,是可行的。如何得知req1完全执行完毕呢?这个就不得而知了。还是强行来一个案例吧

   // 第一次刷新页面只执行req1
   var req1 = window.indexedDB.open(name, 1)
   req1.onupgradeneeded = function(){createMemberStore()}
   // var req2 = window.indexedDB.open(name, 2)
   // req2.onupgradeneeded = function(){createSpendStroe()}
   
   // 第二次刷新页面只执行req2
   // var req1 = window.indexedDB.open(name, 1)
   // req1.onupgradeneeded = function(){createMemberStore()}
   var req2 = window.indexedDB.open(name, 2)
   req2.onupgradeneeded = function(){createSpendStroe()}
   
   // 按照如上步骤可以保证正确执行,但这太苛刻了,不可取。

正确的结论

✅正确的做法,我们应该在onupgradeneeded事件中一次性创建所有的store,version的upgrade是为了改变更新这些store。(其实想想mysql也是先建表格,再做操作)

   // 正确案例
   var req1 = window.indexedDB.open(name, 1)
   req1.onupgradeneeded = function(){
     createMemberStore()
     createSpendStore()
   }

安全的升级version

升级version几条限制

  • 如果存在了store,再次创建,抛错
  • 如果不存在store,删除store,抛错
  • 如果删除store,数据全部丢失,如果想保存数据,需要在version update之前__读取__需要的数据,自行存储,待createStore之后再次写入数据。
    • 这里隐藏了一个暗坑,读取必须在version update之前或者在version update之中读取oldVersion的db的数据(不能在onupgradeneeded中读取当前db的数据,这将存在version update的transition和读取的transition的冲突,并且当前db也没有数据啊😹😹😹😹😹😹)
  • 升级version后,清除站点数据,以免新的version版本不生效

总结

在踩了上面那些坑之后,实现一个方便好用的IDB class(使用了idb package,解决回调地狱),如下:

// db.js
import { openDB } from 'idb';

export default class IDB {
  constructor({ name, version } = {}) {
    this.db = null;
    this.cacheData = null;
    this.name = name;
    this.version = version;
    this.stores = [];
  }

  // 先存储,待创建完成后,setItems
  transfer = async ({ db, storeName } = {}) => {
    // 如果存在,先读取数据,内存缓存,待创建完成后,恢复数据

    this.cacheData = await this.getItems({ storeName, db });
    db.deleteObjectStore(storeName);
  }

  // 从transfer中恢复数据
  recovery = async ({ db, storeName } = {}) => {

    await this.setItems({ storeName, values: this.cacheData, db });
  }


  addStore = ({ storeName, keepData, updated, createStore } = {}) => {
    this.stores.push({ storeName, keepData, updated, createStore });
  }

  // 判断store是否存在
  existStore = ({ db, storeName } = {}) => {
    db = db || this.db
    if (db) {
      return db.objectStoreNames.contains(storeName)
    }
    return false;
  }

  // 创建表格
  createDatabase = async () => {
    this.db = await openDB(this.name, this.version, {
      upgrade: async (db, oldVersion, newVersion, transaction) => {
        await Promise.all(this.stores.map(async obj => {
          const { storeName, keepData, updated, createStore } = obj;

          if (this.existStore({ db, storeName })) {
            // 如果更新版本需要updated,则更新之,否则do nothing
            if (updated) {
              if (keepData) { // 如果保留数据,转移数据,创建store,回复数据
                // 这里一定要用oldVersion打开oldDB,来读取之前的数据
                const oldDb = await openDB(this.name, oldVersion)
                await this.transfer({ db: oldDb, storeName });
                createStore({ db });
                await this.recovery({ db, storeName });
              } else { // 如果不保存数据,直接删除
                db.deleteObjectStore(storeName);
                createStore({ db });
              }
            }
          } else {
            createStore({ db })
          }
        }));
      }
    })
  }

  setItems = async ({ storeName, values, db } = {}) => {
    db = db || this.db;

    if (db && this.existStore({ db, storeName })) {

      if (Object.prototype.toString.call(values) === '[object Object]') {
        // 单个增加
          db.add(storeName, values)
      } else {
        // 多个增加
        const tx = db.transaction(storeName, 'readwrite');
        const arr = values.map(item => {
          return tx.store.add(item);
        })
        arr.push(tx.done)
        await Promise.all(arr)
      }
    }
  }

  editItems = async ({key, values, db, storeName}={})=>{
    db = db || this.db;

    if (db && this.existStore({ db, storeName })) {//判断是否存在store
      const store = db.transaction(storeName, 'readwrite').objectStore(storeName);
      const _data = await store.get(key)
      Object.keys(values).map(k=>{
        if(_data[k] !== values[k]){
          _data[k] = values[k]
        }
      })
      store.put(_data)
    }
  }

  getFromIndex = async({store, key, index, fuzzy}={})=>{
    const _index = store.index(index);
    // _index.get(key) 默认只能获取第一个元素,如果有多个元素需要使用cursor
    // return await _index.get(key);

    let cursor = await _index.openCursor();
    const _values = [];
    while (cursor) {
      if (fuzzy) {
        if (cursor.value[index].indexOf(key) > -1) {
          _values.push(cursor.value);
        }
      } else {
        if (cursor.value[index] === key) {
          _values.push(cursor.value);
        }
      }

      cursor = await cursor.continue()
    }
    return _values
  }
  // 返回数组的结构
  getItems = async ({ storeName, key, db, index, indexName, fuzzy } = {}) => {
    db = db || this.db;

    if (db && this.existStore({ db, storeName })) {//判断是否存在store
      if (key) {
        const store = db.transaction(storeName).objectStore(storeName);
        if(index){
          let _values = []
          if(Object.prototype.toString.call(index) === '[object Array]'){
            const __values = await Promise.all(index.map(async item=>{
              return await this.getFromIndex({ store,key, index: item, fuzzy })
            }))
            _values = __values.flat()
          }else{
            _values = await this.getFromIndex({store,key, index, fuzzy})
          }
          return _values;
        }else{
          return [await store.get(key)]
        }
      } else {
        return await db.getAllFromIndex(storeName, indexName)
      }
    }
  }

  delItems = async ({storeName, key, db}={})=>{
    db = db || this.db;

    if (db && this.existStore({ db, storeName })) {//判断是否存在store
      const store = db.transaction(storeName, 'readwrite').objectStore(storeName);
      await store.delete(key);
    }
  }

  getStoreInstance = ({ storeName, indexName } = {}) => {
    return {
      setItems: async (values) => {
        return await this.setItems({ storeName, values })
      },
      getItems: async (key, index, fuzzy) => {
        return await this.getItems({ storeName, key, index, indexName, fuzzy })
      },
      editItems: async(key, values)=>{
        return await this.editItems({key, storeName, values})
      },
      delItems: async(key)=>{
        return await this.delItems({key, storeName})
      }
    }
  }
}

用法

// biz.js
import IDB from 'db'

const idb = new IDB({ version: 23, name: 'ziyi' })

idb.addStore({
  storeName: 'member',
  keepData: true,
  updated: false,
  createStore: ({ db }) => {

    const store = db.createObjectStore('member', {
      keyPath: 'id',
      autoIncrement: true
    });
    store.createIndex('phone', 'phone', { unique: true });
    store.createIndex('name', 'name');
    store.createIndex('score', 'score');
    store.createIndex('hisotry', 'history');
  }
});

idb.addStore({
  storeName: 'spend',
  updated: false,
  createStore: ({ db }) => {
    const store = db.createObjectStore('spend', {
      keyPath: 'id',
      autoIncrement: true
    })

    store.createIndex('phone', 'phone');
    store.createIndex('desc', 'desc');
  }
});



const member = idb.getStoreInstance({ storeName: 'member', indexName: 'phone' })
const spend = idb.getStoreInstance({ storeName: 'spend', indexName: 'id' })

await idb.createDababase();

await member.getItems();
await member.setItems({phone: xxxx})
awiat member.setItems([{phone: xxxx},{}])