如何优雅的封装indexedDB
前言
本篇文章不会过多介绍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>
如果我们直接运行上面的代码,会发现控制台报错
实际上,新建或者打开数据库的过程都是异步的,我们需要等待数据库打开后,拿到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)
刷新页面,可以发现,数据插入成功
将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是否连接完成
转载自:https://juejin.cn/post/7244505316233936952