目录
✨前言
由于公司项目的订单系统架构中需要前端存储大量数据,然后分批次传给后端进行处理,所以一个前端存储技术的使用和探索就势在必行了。对于存储有这几方面的需求,一是可以存储大量的数据起码可以10W条起步(有客户账号下有存在上万数据的情况),二是需要有表的概念(因为需要存储不同业务类型的数据),三是需要支持分页(因为前期数据都是存储到前端的有可能涉及到大量数据的分页部分)。所以查阅了相关资料后indexedDB数据库就映入眼帘。
✨直通车
点此处跳过介绍内容直达数据库使用篇
✨indexedDB简介
引用MDN官网的介绍
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。
IndexedDB 是一种在用户浏览器中持久存储数据的方法。因为它允许您创建具有丰富查询能力的 Web 应用程序,而不管网络可用性如何,这些应用程序可以在线和离线工作。IndexedDB 对于存储大量数据的应用程序(例如,借出图书馆中的 DVD 目录)和不需要持久 Internet 连接才能工作的应用程序(例如,邮件客户端、待办事项列表和记事本)。
IndexedDB 允许您存储和检索使用“键”索引的对象。您对数据库所做的所有更改都发生在事务中。与大多数 Web 存储解决方案一样,IndexedDB 遵循同源策略。因此,虽然您可以访问域内存储的数据,但不能访问不同域中的数据。
通过以上介绍大体了解他支持大数据存储、支持事务操作,而其他前端常用的存储技术对于大量数据存储这件事上都有点爱莫能助了。
客户端存储技术方案对比:
特性 | cookie | localStorage | sessionStorage | IndexedDB |
---|---|---|---|---|
生命周期 | 设置过期时间,到期清楚 | 除非手动情况,否则永久保存 | 关闭浏览器失效 | 除非手动情况,否则永久保存 |
存储大小 | 4k | 5-10M | 5-10M | 不少于 250MB |
与服务端通信 | 可以 | 无 | 无 | 无 |
访问策略 | 符合同源策略可以访问 | 符合同源策略可以访问 | 符合同源策略可以访问 | 符合同源策略可以访问 |
✨ indexedDB功能特点
一、非关系型数据库(NoSql)
所有类型的数据都可以直接存入,包括 JavaScript 对象、二进制数据(ArrayBuffer 对象和 Blob 对象),数据以key-value形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
二、持久化存储
这里的持久化存储是打引号的,indexed不算是真正的持久化,他只是比cookie、localStorage、sessionStorage等方式存储的数据难以清除一点而已。浏览器默认清除缓存内容是不包括indexedDB的,但是如果在清除的时候设置好全部清除记录还是会删除掉的。以FireFox为例:
三、全异步操作
IndexedDB天然支持异步的,所有操作都是基于异步,不会锁死浏览器,用户依然可以进行其他的操作。当然是优点也是缺点,异步操作所有的结果处理都得放到回调中代码写起来阅读起来都很别扭(不过有解决方案,后面会提到)
四、支持事务
indexedDB天然支持事务操作,基本保证了数据的一致性。并提供了三种事务模式,readOnly(只读,默认)、readwrite(读写)versionchange(数据库版本变化),前两中最为常用。
✨方案优缺点分析
优点
- 支持大量数据存储(官方文档描述可存储量根据本机硬盘容量而定)。可用windowAPI查询
const quota = await navigator.storage.estimate();
// quota.usage -> 已用字节数。
// quota.quota -> 最大可用字节数。
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`您已使用可用存储的 ${percentageUsed}%。`);
const remaining = quota.quota - quota.usage;
console.log(`您最多可以再写入 ${remaining} 个字节。`);
- 支持大批量数据处理。由于原来后端接口限制大批量操作,故一次只能让用户导入2000张卡。采用此方案可以前端批量导入,然后调用后端单数据处理接口并且前端使用进度条提示用户当前处理进度
- 使其后端逻辑简化,将来新增的业务开发周期短,提高了满足产品需求的能力。
缺点
- 硬件配置方面限制:此方案要求用户不能使用性能过低的电脑,若使用性能过低的电脑,可能会报错。推荐客户暂时关闭无关的应用。
- 浏览器方面限制:不能使用太过偏门的浏览器,常见的浏览器都没有问题。推荐用户使用谷歌、360、qq、2345浏览器。
- 用户行为方面限制:办理中用户如果深度清除浏览器缓存,会造成数据丢失。推荐用户办理过程中不要清除浏览器缓存,或清除了就重新申请。
读写速度测试
数据量 | 1W | 5W | 10W |
---|---|---|---|
写入时间 | 2s | 10s | 24s |
读取时间 | 22ms | 111ms | 235ms |
✨indexedDB常用操作(demo)
创建数据库
const dbName = "test_duxinli_DB";
var db;
var request = indexedDB.open(dbName, 2);
request.onerror = function(event) {
// 错误处理程序在这里。
};
request.onupgradeneeded = function(event) {
db = event.target.result;
// 创建一个对象存储空间来持有有关我们客户的信息。
// 我们将使用 "iccid" 作为我们的 key path 因为它保证是唯一的。
var objectStore = db.createObjectStore("cradList", { keyPath: 'iccid', autoIncrement: true });
// 创建一个索引来通过 name 搜索客户。
// 可能会有重复的,因此我们不能使用 unique 索引。
objectStore.createIndex("name", "name", { unique: false });
// 创建一个索引来通过 email 搜索客户。
// 我们希望确保不会有两个客户使用相同的 email 地址,因此我们使用一个 unique 索引。
objectStore.createIndex("imei", "imei", { unique: false });
插入数据
// 建立读写事务,向对象仓库写入数据记录
let request = this.db.transaction(['cradList'], 'readwrite').objectStore('cradList').add({
iccid: '',
name: '',
imei: '',
})
request.onsuccess = event => {
console.log("数据写入成功");
};
request.onerror = event => {
this.notify('数据写入失败')
读取数据
这里列举三个实际项目中可能会遇到的读取数据的场景,分别是通过主键读取数据、通过游标读取数据、通过索引读取数据
主键读取
var transaction = db.transaction(['cradList']); // 事务
var objectStore = transaction.objectStore('cradList'); // 仓库对象
var request = objectStore.get(key); // 通过主键获取数据
request.onerror = function (event) {
console.log("事务失败");
};
request.onsuccess = function (event) {
console.log("主键查询结果: ", request.result);
};
游标读取
let list = [];
var store = db
.transaction(['cradList'], "readwrite") // 事务
.objectStore('cradList'); // 仓库对象
var request = store.openCursor(); // 指针对象
// 游标开启成功,逐行读数据
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
// 必须要检查
list.push(cursor.value);
cursor.continue(); // 遍历了存储对象中的所有内容
} else {
console.log("游标读取的数据:", list);
}
};
索引读取
var store = db.transaction(['cradList'], "readwrite").objectStore('cradList');
var request = store.index(indexName).get(indexValue);
request.onerror = function () {
console.log("事务失败");
};
request.onsuccess = function (e) {
var result = e.target.result;
console.log("索引查询结果:", result);
};
分页查询
由于indexedDB没有提供分页查询的api所以只能通过迂回的方式实现
/**
* 通过索引和游标分页查询记录
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} indexName 索引名称
* @param {string} indexValue 索引值
* @param {number} page 页码
* @param {number} pageSize 查询条数
*/
function cursorGetDataByIndexAndPage(
db,
storeName,
indexName,
indexValue,
page,
pageSize
) {
let list = [];
let counter = 0; // 计数器
let advanced = true; // 是否跳过多少条查询
var store = db.transaction(['cradList'], "readwrite").objectStore('cradList'); // 仓库对象
var request = store
.index(indexName) // 索引对象
.openCursor(IDBKeyRange.only(indexValue)); // 指针对象
request.onsuccess = function (e) {
var cursor = e.target.result;
if (page > 1 && advanced) {
advanced = false;
cursor.advance((page - 1) * pageSize); // 跳过多少条
return;
}
if (cursor) {
// 必须要检查
list.push(cursor.value);
counter++;
if (counter < pageSize) {
cursor.continue(); // 遍历了存储对象中的所有内容
} else {
cursor = null;
console.log("分页查询结果", list);
}
} else {
console.log("分页查询结果", list);
}
};
request.onerror = function (e) {};
}
更新数据
var request = db
.transaction(['cradList'], "readwrite") // 事务对象
.objectStore('cradList') // 仓库对象
.put(data);
request.onsuccess = function () {
console.log("数据更新成功");
};
request.onerror = function () {
console.log("数据更新失败");
};
删除数据
var request = db
.transaction(['cradList'], "readwrite") // 事务对象
.objectStore('cradList') // 仓库对象
.delete(id);
request.onsuccess = function () {
console.log("数据删除成功");
};
request.onerror = function () {
console.log("数据删除失败");
};
✨第三方类库dexie.js
这是什么
这是一个针对于indexedDB封装的第三方类库,集成了CRUD等基本常用操作,让我们可以想操作mysql这些数据库一样优雅
可以解决什么问题
- indexedDB原生的操作都是在回调中进行的,书写起来不是很优雅
- 创建链接建库建表实现起来很是繁琐(由于业务需要建立多张表)
- 对于分页查询这种比较复杂的操作,原生支持的不好,需要通过其他方式实现
针对以上问题,dexie.js都很好的解决了。而且是基于promise实现,还提供诸多丰富的接口,包含了项目中可以遇到的操作
如何使用
- 安装dexie.js
npm install dexie
- 快捷的创建库,需要一行代码就可以创建好数据库
var db = new Dexie(dbname); //创建数据库
- 快速创建多张表和对应字段
db.version(lastVersion).stores(
{
localVersions: 'matadataid, content, lastversionid, date, time',
users: "++id, name, &username, *email, address.city",
relations: "++rid, userId1, userId2, [userId1+userId2], relation",
books: 'id, author, name, *categories'
}
);
- 添加操作
const id = await db.friends.add({
name: this.friendName,
age: this.friendAge,
});
- 分页
const someFriends = await db.friends
.where("categories").between(20, 25)
.offset(150).limit(25)
.toArray();
具体更多用法可以参考一下文档:
官方文档
其他人整理的使用案例
💖此处我也封装好了一个简单的调用demo,如需要的老铁可以点赞+关注私信我领取💖
✨总结
经过预研发现在前端处理大量数据的做法是可行的,利用上dexie.js的加持,是可以满足业务逻辑的需要的。而且在数据存储量和读写速度上经过测试也是可以满足需要的。