使用Promise封装IndexedDB,你学会了吗?

3,861 阅读5分钟

使用Promise封装IndexedDB

IndexedDB是大型NoSQL存储系统。它使你几乎可以将任何内容存储在用户的浏览器中。除了通常增删改查外,IndexedDB还支持事务。

为何使用IndexdDB

IndexedDB 具有以下特点

  1. IndexedDB 数据库存储键值对。 IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
  2. IndexedDB API主要是异步的。 IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
  3. IndexedDB 构建在事务数据库模型上。 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
  4. 同源限制 IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
  5. 储存空间大 IndexedDB 的储存空间比 LocalStorage 大得多,全局限制为硬盘的50%,组的限制为硬盘的20%。某些浏览器会限制每次写入130M
  6. 支持二进制储存。 IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。

原生过程实现

IndexedDB 虽好但是用法没有LocalStorage 简单,我们先来看下步骤与写法。

  1. 首先打开数据库 -> indexedDB.open() -> IDBDatabase
  2. 开始一个事务 -> IDBDatabase.transaction() -> IDBTransaction ->IDBObjectStore
  3. 新建数据库 (IDBObjectStore.createObjectStore())
  4. 新增数据 (IDBObjectStore.add())、读取数据 (IDBObjectStore.get())、更新数据 (IDBObjectStore.put())、删除数据 (IDBObjectStore.delete())
  5. 遍历数据 (IDBObjectStore.openCursor())

1.打开数据库

// IDBOpenDBRequest  表示打开数据库的请求
const request: IDBOpenDBRequest  = indexedDB.open( databaseName, version );
// 版本更新,创建新的store的时候
request.onupgradeneeded = ( event ) => {
 // // IDBDatabase 表示与数据库的连接。这是获取数据库事务的唯一方法。
 const db: IDBDatabase = event.target.result;
 if ( db.objectStoreNames.contains( stroeName ) === false ) {
   db.createObjectStore( stroeName, {keyPath: 'key'} );
 }
 openSuccess(db);
};
request.onsuccess = ( event ) => {
 // IDBDatabase 表示与数据库的连接。这是获取数据库事务的唯一方法。
 const db: IDBDatabase = event.target.result;
 openSuccess(db);
};
request.onerror = ( event ) => {
 console.error( 'IndexedDB', event );
};

2.开始一个事务

function openSuccess (db: IDBDatabase) {
 transaction: IDBTransaction = db.transaction( [ storeName ], 'readwrite' );
}

3.获取IDBObjectStore

function openSuccess (db: IDBDatabase) {
 transaction: IDBTransaction = db.transaction( [ storeName ], 'readwrite' );
 objectStore: IDBObjectStore = transaction.objectStore(storeName);
}

4.增删改查以 新增为例

const request: IDBRequest = objectStore.add(data);
request.onsuccess = function ( event ) {};
request.onerror = function ( event ) {};

5.遍历数据

const request = objectStore.openCursor();
request.onsuccess = ( event ) => {
 let cursor = event.target.result;
 // 没有数据可遍历时 cursor === null
 if (cursor !== null) {
   callback(cursor.value);
   // 下一个
   cursor.continue();
 }
};
request.onerror = (event) => {
 callback(event)
}

原生写法使用效果:

2546b1d8ceb582a0.gif 原生写法缺点

  • 获取objectStrore路径过长
  • 回调函数过多
  • 扩展性差

所以我们来尝试使用Promise + class 封装 IndexedDB API

Promise代码实现

其实关键点是 objectStore获取比较复杂,经过open,transaction。我们获取后将这个对象缓存下来,然后增删改查操作都通过一个getObjectStore方法获取ObjectStore对象。

整个结构如下:

class DB {
 constructor(databaseName, version, storeOptions) {}
 /**
  * 打开数据库
  */
 open(databaseName, version, storeOptions) {}
 async _getTransaction(storeName, version) {}
 /**
  * 获取store
  */
 async _getObjectStore(storeName, version) {}
 /**
 * 获取一个store
 */
 collection(storeName, version) {
   this.currentStore = storeName;
   this._getObjectStore(storeName, version);
   return this;
 }
 async get(data) {}
 async add(data) {}
 async delete(data) {}
 async put(data) {}
 async clear(storeName) {}
 async each(callback) {}
}

上面提到获取ObjectStore需要通过transaction获取,而transaction通过indexedDB.open() 获取

首先初始化一下重要参数

constructor(databaseName, version, storeOptions) {
 // 缓存数据库名称:name + version为key
 this._dbs = {};
 this._databaseName = databaseName;
 this.open(databaseName, version, storeOptions);
}

open需要监听onupgradeneeded ,版本更新时会进入触发此事件,这种情况要创建好store和主键,不然事务那边会获取失败

open(databaseName, version, storeOptions) {
   return new Promise((resolve, reject) => {
     // 有缓存的情况
     if (this._dbs[databaseName + version]) {
       resolve(this._dbs[databaseName + version]);
       return;
     }

      const request = indexedDB.open(databaseName, version);
       // 版本更新,创建新的store的时候
      request.onupgradeneeded = (event) => {
          // IDBDatabase
         const database = event.target.result;
         // 缓存起来
         this._dbs[databaseName + version] = database;
         for (const key in storeOptions) {
           if (database.objectStoreNames.contains(key) === false) {
             const keyPath = storeOptions[key] ? storeOptions[key] : [];
             database.createObjectStore(key, { keyPath });
            }
         }
         resolve(database);
      };
     request.onsuccess = (event) => {
       // IDBDatabase
       const database = event.target.result;
       // 缓存起来
       this._dbs[databaseName + version] = database;
       resolve(database);
     };
     request.onerror = (event) => {
       reject(event);
       console.error('IndexedDB', event);
     };
   });
}

接下来是获取事务并通过事务得到ObjectStore对象

async _getTransaction(storeName, version) {
 let db;
 // 先从缓存获取
 if (this._dbs[this._databaseName + version]) {
   db = this._dbs[this._databaseName + version];
 } else {
   db = this.open(this._databaseName, version);
 }
 return db.transaction( [ storeName ], 'readwrite' );
}

/**
 * 获取store
 * ObjectStore: 表示允许访问IndexedDB数据库中的一组数据的对象存储,
 */
async _getObjectStore(storeName, version) {
 let transaction = await this._getTransaction(storeName, version);
 return transaction.objectStore( storeName );
}

拿到ObjectStore后可以直接操作增删改查了,这几个方法比较简单,通过Promise包一层就行

get(data) {
   return new Promise((resolve, reject) => {
     this._getObjectStore(this.currentStore).then((objectStore) => {
       const request = objectStore.get(data);
       request.onsuccess = function ( event ) {
         resolve(event.target.result);
       };
       request.onerror = (event) => {
         reject(event);
       }
      });
   });
}

add(data) {
 return new Promise((resolve, reject) => {
     this._getObjectStore(this.currentStore).then((objectStore) => {
       const request = objectStore.add(data);
       request.onsuccess = function ( event ) {
         resolve(event.target.result);
       };
       request.onerror = (event) => {
         reject(event);
       }
     });
  });
}

delete(data) {
 return new Promise((resolve, reject) => {
     this._getObjectStore(this.currentStore).then((objectStore) => {
       const request = objectStore.delete(data);
       request.onsuccess = function ( event ) {
         resolve(event.target.result);
       };
       request.onerror = (event) => {
         reject(event);
       }
     });
  });
}

put(data) {
 return new Promise((resolve, reject) => {
     this._getObjectStore(this.currentStore).then((objectStore) => {
       const request = objectStore.put(data);
       request.onsuccess = function ( event ) {
         resolve(event.target.result);
       };
       request.onerror = (event) => {
         reject(event);
       }
     });
  });
}

clear(storeName) {
 return new Promise((resolve, reject) => {
     this._getObjectStore(this.currentStore).then((objectStore) => {
       const request = objectStore.clear(data);
       request.onsuccess = function ( event ) {
         resolve(event.target.result);
       };
       request.onerror = (event) => {
         reject(event);
       }
     });
  });
}

成果展示

最终使用起来就简洁了很多

  • 简化创建一个DB的步骤
  • 缓存了objectStore
  • API Promise化,使用更简洁

2cc4a1c8870e16b2.gif

import DB from './BD';

/* 打开数据库并开始一个事务 */
// open -> transaction -> objectStore
const db = new DB('db', { store: 'id' });

const store = db.collection('store');

const data = [
    {
 name: '甲',
 id: 100,
    },
    {
 name: '乙',
 id: 1001,
    },
];

/* 增 */
store.add(data[1]).then(ev => {  
 store.add(data[0])
 store.add(data[1])
});

/* 删 */
store.delete(data[0].id).then(ev => {
 console.log(ev);
});

/* 改 */
store.put(data[0]).then((ev) => {
 // store.put(data[0]).then(ev => {
 // });
 store.get(data[0].id).then((result) => {
   console.log(result);
 });
});

/* 查 */
store.get(data[0].id).then((result) => {
 console.log(result);
});

/* 遍历 */
store.each((result) => {
 console.log(result);
});

总结

如果碰到前端频繁存储操作或者大文件缓存的需求,可以考虑使用IndexedDB。当然项目中推荐直接使用第三方库zangodbdexie.js API更丰富。