likes
comments
collection
share

如何优雅的封装indexedDB

作者站长头像
站长
· 阅读数 16

前言

本篇文章不会过多介绍indexedDB基础用法,而是介绍一种尽可能优雅的方式封装indexedDB,方便在业务中使用。采用类似localStorage的用法,降低indexedDB的使用门槛。同时,通过本节案例,来看看resolvablePromise是如何增强异步编程的能力。

简介

我们先来看下indexedDB的简单用法

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <title>IndexedDB</title>
</head>

<body>
    <div>hello IndexedDB</div>
    <script>
        const dbName = "mydatabase";
        const storeName = "product-table";
        // 1.新建或者打开数据库
        const request = window.indexedDB.open(dbName);
        let db;
        request.onupgradeneeded = () => {
            db = request.result;
            // 2.创建对象仓库,即新建表
            db.createObjectStore(storeName);
        };
        request.onsuccess = (event) => {
            db = request.result;
            console.log("onsuccess...", db);
        };
        // 3.添加数据
        const setItem = (key, value) => {
            const store = db
                .transaction(storeName, "readwrite")
                .objectStore(storeName)

            const request = store.put(value, key)
            request.onsuccess = function () {
                console.log("数据写入成功", request.result);
            };

            request.onerror = function () {
                console.log("数据写入失败", request);
            };
        };
        // 4.读取数据
        const getItem = (key) => {
            const store = db
                .transaction(storeName, 'readonly')
                .objectStore(storeName);

            const request = store.get(key);
            request.onerror = function () {
                console.log('读取失败', request);
            };
            request.onsuccess = function () {
                console.log('读取数据成功...', request.result)
            };
        }

        // 使用
        setItem('1', 'abc');
        setItem('2', 'bcd');
    </script>
</body>

</html>

如果我们直接运行上面的代码,会发现控制台报错

如何优雅的封装indexedDB

实际上,新建或者打开数据库的过程都是异步的,我们需要等待数据库打开后,拿到db实例,才能操作数据库。在本例中,我们同步调用了setItem方法,此时数据库还没打开成功,db实例没有实例化完成,因此我们在setItem方法中调用db.transaction就会报错。因此,我们可以将setItem的调用放在onSuccess回调里,即:

request.onsuccess = (event) => {
  db = request.result;
  console.log("onsuccess...", db);
  // 使用
  setItem('1', 'abc');
  setItem('2', 'bcd');
};

当然你也可以放在setTimeout中调用,比如:

request.onsuccess = (event) => {
  db = request.result;
  console.log("onsuccess...", db);
};
//....
setTimeout(() => {
 // 使用
 setItem('1', 'abc');
 setItem('2', 'bcd');
}, 1000)

刷新页面,可以发现,数据插入成功

如何优雅的封装indexedDB

setItem的调用放在onSuccess回调里面虽然能够解决了db实例化的问题。但对使用方来说很不友好,存在以下痛点:

  • 1.setItem等方法没法安全调用。我们希望的是能够在业务里面任何时候任何地方随时调用,调用方无需考虑db实例化的问题,降低调用方心智负担,提高灵活性。
  • 2.setItem和getItem方法有大量的模版代码,比较冗余。
  • 3.拓展性不强,同时在使用上不像localstorage那般灵活

下面,我们逐步解决以上问题。

resolvablePromise

我们先来下resolvablePromise。实际上就是一个简单的生成promise的方法。

const resolvablePromise = () => {
  let resolve;
  let reject;
  const promise = new Promise((_resolve, _reject) => {
    // 注意以下两行代码
    resolve = _resolve;
    reject = _reject;
  );
  promise.resolve = resolve;
  promise.reject = reject;
  return promise;
};
const pro = resolvablePromise()
// 改变promise状态
pro.resolve('hello world')

这个方法没有什么特殊魔法,生成一个新的promise,并将promise的resolve和reject方法挂在到promise实例上。

resolvablePromise虽然简单,但是如果能够合理在业务中使用,能够发挥异步编程的极大作用。下面我们来看下resolvablePromise如何结合indexedDB解决indexedDB异步编程的痛点。

如何巧妙解决indexedDB异步编程痛点

使用resolvablePromise可以帮助我们巧妙解决大部分异步编程的痛点,包括indexedDB。改造上面的代码:

const resolvablePromise = () => {
  let resolve;
  let reject;
  const promise = new Promise((_resolve, _reject) => {
    resolve = _resolve;
    reject = _reject;
  });
  promise.resolve = resolve;
  promise.reject = reject;
  return promise;
};

const dbName = "mydatabase";
const storeName = "product-table";
// 1.新建或者打开数据库
const request = window.indexedDB.open(dbName);
const dbPromise = resolvablePromise();
request.onupgradeneeded = () => {
  const db = request.result;
  // 2.创建对象仓库,即新建表
  db.createObjectStore(storeName);
  dbPromise.resolve(db);
};
request.onsuccess = (event) => {
  const db = request.result;
  console.log("onsuccess...", db);
  dbPromise.resolve(db);
};
// 3.添加数据
const setItem = async (key, value) => {
  const db = await dbPromise;
  const store = db.transaction(storeName, "readwrite").objectStore(storeName);

  const request = store.put(value, key);
  request.onsuccess = function () {
    console.log("数据写入成功", request.result);
  };

  request.onerror = function () {
    console.log("数据写入失败", request);
  };
};
// 4.读取数据
const getItem = async (key) => {
  const db = await dbPromise;
  const store = db.transaction(storeName, "readonly").objectStore(storeName);

  const request = store.get(key);
  request.onerror = function () {
    console.log("读取失败", request);
  };
  request.onsuccess = function () {
    console.log("读取数据成功...", request.result);
  };
};

setItem("3", "hello 3");

getItem("3");

上面的代码,有几处改动:

  • 1.我们使用resolvablePromise方法生成一个dbPromise
  • 2.在request.onupgradeneeded和request.onsuccess回调中调用dbPromise.resolve(db)改变dbPromise的状态。
  • 3.修改getItem和setItem方法,加个async,同时在方法中调用const db = await dbPromise获取db实例

经过几点小改动,我们就可以放心、安全、没有心智负担的在任何地方调用setItem或者getItem方法了,调用方完全不需要关心db什么时候实例化完成。如果db没有实例化完成,那么setItem或者getItem中的const db = await dbPromise会一直等到db实例化完成后才会执行后续的操作语句。

干掉模版代码,精简代码

由于setItem、getItem方法中存在相似的模版代码,因此我们可以考虑将这部分代码封装一下。

const resolvablePromise = () => {
  let resolve;
  let reject;
  const promise = new Promise((_resolve, _reject) => {
    resolve = _resolve;
    reject = _reject;
  });
  promise.resolve = resolve;
  promise.reject = reject;
  return promise;
};

const dbName = "mydatabase";
const storeName = "product-table";
// 1.新建或者打开数据库
const request = window.indexedDB.open(dbName);
const dbPromise = resolvablePromise();
request.onupgradeneeded = () => {
  const db = request.result;
  // 2.创建对象仓库,即新建表
  db.createObjectStore(storeName);
  dbPromise.resolve(db);
};
request.onsuccess = (event) => {
  const db = request.result;
  console.log("onsuccess...", db);
  dbPromise.resolve(db);
};
const getStore = async (operationMode) => {
  const db = await dbPromise;
  const store = db.transaction(storeName, operationMode).objectStore(storeName);
  return store;
};
const promisify = (request) => {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
};
// 3.添加数据
const setItem = async (key, value) => {
  const store = await getStore("readwrite");

  const request = store.put(value, key);
  return promisify(request);
};
// 4.读取数据
const getItem = async (key) => {
  const store = await getStore("readonly");

  const request = store.get(key);
  return promisify(request);
};

setItem("3", "hello 3");

getItem("3").then((res) => {
  console.log("get..", res);
});

以上代码有几点改动的地方:

  • 1.封装getStore方法用于获取数据表实例
  • 2.封装promisify方法,用于将回调方式的request转成promise
  • 3.修改setItem、getItem方法

此时,我们就可以在业务中放心调用。至此,我们已经可以在业务中放心安全的操作indexedDB。

封装dbStorage

这次,我们封装一个dbStorage,能够像localStorage的API一样简洁使用,同时能够灵活的在其他业务中使用

/**
 * 基于IndexDB封装的仿localStage用法的工具
 * **/
function promisify(request) {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}
class DBStorage {
  constructor(dbName, storeName) {
    const request = window.indexedDB.open(dbName);
    request.onupgradeneeded = () => request.result.createObjectStore(storeName);
    this.dbPromise = promisify(request);
    this.storeName = storeName;
  }
  async getStore(operationMode, storeName = this.storeName) {
    const db = await this.dbPromise;
    return db.transaction(storeName, operationMode).objectStore(storeName);
  }
  async setItem(key, value) {
    const store = await this.getStore('readwrite');
    return promisify(store.put(value, key));
  }
  async getItem(key) {
    const store = await this.getStore('readonly');
    return promisify(store.get(key));
  }
  async removeItem(key) {
    const store = await this.getStore('readwrite');
    return promisify(store.delete(key));
  }
  async clear() {
    const store = await this.getStore('readwrite');
    return promisify(store.clear());
  }
}

const dbStorage = new DBStorage('mydatabase', 'product-table');
export default dbStorage;

注意,这里使用了promisify生成dbPromise,没有使用resolvablePromise,原理差不多的。

总结

至此,我们可以像localStorage一样使用indexedDB。通过本篇文章,我们了解到了resolvablePromise在异步编程中能够给我们带来很大的便利。又比如,在下面的业务场景中,我们调用第三方的IMSDK,实例化后得到一个im实例。但是im实例需要等到onConnect回调完成后,即连接完成后,才能调用im.sendSingleMessage方法。也就是说,下面的调用是会报错的

const im = new IMSDK();
im.init({
  uid: uid,
})
  .login()
  .onConnect((data, ctx) => {
      console.log('连接成功'a)
  });

im.sendSingleMessage({
  data,
  onSuccess: (data) => {},
  onFail: (e) => {},
  onTimeout: (e) => {},
});

我们应该要这样调用:

const im = new IMSDK();
im.init({
  uid: uid,
})
  .login()
  .onConnect((data, ctx) => {
    im.sendSingleMessage({
      data,
      onSuccess: (data) => {},
      onFail: (e) => {},
      onTimeout: (e) => {},
    });
  });

问题来了,我们在业务代码中,随时都会调用im.sendSingleMessage发送消息,调用方不需要关注im连接是否完成。我们可以借助resolvablePromise解决这个问题。

const imPromise = resolvablePromise();

const im = new IMSDK();
im.init({
  uid: uid,
})
  .login()
  .onConnect((data, ctx) => {
    imPromise(im);
  });

const sendSingleMessage = async (data) => {
  const im = await imPromise;
  im.sendSingleMessage({
    data,
    onSuccess: (data) => {},
    onFail: (e) => {},
    onTimeout: (e) => {},
  });
};

调用方只需要随时调用sendSingleMessage即可,不用关注im是否连接完成