2024年,前端主流的保存数据方案有三种:
- Cookie
- LocalStorage
- IndexedDB
一般情况下都是使用前两种,第三种IndexedDB很少会用到,但是要保存大量数据的话,IndexedDB是唯一选择。
Cookie的存储空间有4KB,LocalStorage的存储空间有10M,而IndexedDB最少有250MB。
但是,真的不要用IndexedDB,因为它太坑了。
一. 异步API太难用
IndexedDB从连接数据库开始的API都是异步的,这会导致代码逻辑非常复杂。
以连接数据库为例:
const request = window.indexedDB.open("MyTestDatabase", 3);
request.onerror = (event) => {
// Do something with request.errorCode!
};
request.onsuccess = (event) => {
// Do something with request.result!
};
在调用open函数后,会返回一个异步的request,然后需要注册回调函数 onsuccess和onerror。onsuccess代表成功打开数据库,onerror代表打开数据库失败,需要处理这两种情况。
除了连接数据库,读数据库的内容也是异步的,为了方便使用,可以转成promise,然后使用 await来获取数据,如下所示:
// Let us open our database
const DBOpenRequest = window.indexedDB.open("toDoList", 4);
DBOpenRequest.onsuccess = (event) => {
// store the result of opening the database in the db variable.
db = DBOpenRequest.result;
// Run the getData() function to get the data from the database
getData();
};
function getData() {
// open a read/write db transaction, ready for retrieving the data
const transaction = db.transaction(["toDoList"], "readwrite");
// create an object store on the transaction
const objectStore = transaction.objectStore("toDoList");
// Make a request to get a record by key from the object store
const objectStoreRequest = objectStore.get("Walk dog");
return new Promise((resolve, reject) => {
objectStoreRequest.onsuccess = (event) => {
resolve(objectStoreRequest.result);
};
objectStoreRequest.onerror = () => {
reject(objectStoreRequest.error);
}
});
}
二. 版本号概念
IndexedDB 有一个版本(version)的概念,连接数据库时就可以指定版本
const version = 1;
const request = window.indexedDB.open('MyTestDatabase', version);
IndexedDB的版本主要用来控制数据库的结构,当数据库结构发生变化时,版本也会发生变化。
在版本改变时会触发onupgradeneeded。在首次连接数据库时,版本从0变成1,也会触发onupgradeneeded,且先于onsuccess。想创建数据表,就可以将它放到这个事件中:
request.onupgradeneeded = function (event) {
db = event.target.result;
if (!db.objectStoreNames.contains('toDoList')) {
db.createObjectStore('toDoList', {
keyPath: 'id', // 主键
autoIncrement: true // 自增
});
}
}
当版本号低于当前存在的版本时,会触发onerror事件,需要重新赋值version再打开。
request.onerror = (e) => {
if (e.target.error && e.target.error.name === 'VersionError') {
// 打开低版本数据库导致失败
try {
const messgae = (e.target).error.message;
const regexp =
/The requested version \([0-9]+\) is less than the existing version \([0-9]\)/;
const isVersionLowError = messgae.search(regexp);
if (isVersionLowError > -1) {
const existVersion = messgae.match(/[0-9]+/gi)[1];
if (existVersion > version) {
version = existVersion;
}
}
} catch (e) {
console.log(e);
}
}
}
三. 事务操作太繁琐
IndexedDB 里面的事务,保证了所有操作是按照一定的顺序进行,不会导致同时写入的问题。另外,IndexedDB 里面,强制规定了,任何读写操作,都必须在一个事务中进行。
特别是写操作,事务通过尝试性写入完成第一步更新,如果整个尝试过程没有出错,那么会通过一个 commit 操作使得整个更新生效。但是如果在尝试过程中出错了,就会进行一个 abort 操作,之前做过的尝试会直接被丢弃,数据不会发生改变。一个 objectStore 的写入过程中,另外一个写入操作会被挂起,直到上一个事务完成。
在代码层面,我们必须通过 transaction 方法,向数据库容器提出事务要求,才能往具体的 objectStore 进行数据处理:
let transaction = db.transaction(['toDoList'], 'readonly')
let objectStore = transaction.objectStore('toDoList')
let request = objectStore.get('111')
一定要注意indexedDB 事务的生命周期。一个事务,它会把你在它的生命周期里面规定的操作全部执行,一旦执行完,周期结束,那么事务就关闭了,你不能再利用这个事务的实例进行下一步操作。
你可以把 indexedDB 里面的事件想象成时间非常短(1ms)的 debounce,如果在这个 1ms 里面,没有新的任务要做,它就关闭了,不能再被使用。
四. Connection经常丢失
当你解决了上面的所有问题后,你以为就没问题了吗?No!No!No!
经过线上检验,DB的连接经常丢失,也就是说,当你open数据库后,再调用get或者add这些方法时,失败了,因为数据库连接丢失了,丢失率大概在3%左右。
虽然可以判断连接断开后,重新连接,但是耗时大大增加,而且代码逻辑变得更加复杂,这对于正常的数据库操作来说,基本是不能接受的。
因此,如果你想用IndexDB来存储数据的话,一定不要存特别重要的数据,存些日志是比较适用的场景。
五. 浏览器兼容性
IndexedDB本身的浏览器兼容性已经很不错了,基本没问题。
由于IndexedDB的API是异步的,如果要使用async/await的话,就要注意一下兼容性了。在module顶层使用await的话,ios15才支持。
此外,由于IndexedDB的操作比较复杂,也有一些库能让操作变简单一些,比如Dexie.js和idb。
Dexie.js
Dexie.js挺好用的,它把所有的API都转成了Promise的返回,并且它不用你担心transaction,它把数据库的相关操作都用transaction包好了,你基本可以像操作LocalStorage一样操作IndexedDB。
此外,它支持了很多便捷的数据库查询操作,用起来非常方便。
但是要特别注意,它只支持ios12.2以上,而且它的库大小为2.95MB,相较于其他库来说还是比较大的。
idb
idb是一个使用非常广泛的库,它主要就是把所有的API都转成了Promise的返回。它非常轻量,浏览器兼容性也很好。不过如果你只是简单地使用IndexedDB,那么自己转Promise也是一样的。
综上所述,一般情况下不建议使用IndexedDB。如果只是存少量的数据,能用LocalStorage最好。如果真的要存很多数据,还是考虑后台存储吧。如果你要存些日志,那再考虑IndexedDB吧,不过记得处理好各种异常情况。