客户端存储 — IndexedDB 实现分页查询
前言
相信 IndexedDB 大家都有过了解,但是不一定每个人都有过实践,并且其中涉及到事务、游标等概念,会导致在初次使用时会有些不适应,那么本文会通过 IndexedDB 实现分页查询的形式进行实践,在开始之前,可以尝试思考一下浏览器的客户端存储你都了解哪些呢?
其实客户端存储分为下面几类:
-
cookie
-
Web Storage
sessionStorage
localStorage
-
IndexDB
cookie
cookies
的特点:
- cookie 是与特定域进行绑定的
- 所有 cookie 都会作为请求头部由浏览器发送给服务器
- 大多数浏览器对 1个域 下所有 cookie 的限制是不超过 4096 Byte(即 4 KB),上下可以有 1 Byte 的误差
假如 cookie 中保存大量信息,那么可能会影响特定域下浏览器请求的性能,因为保存的 cookie 越大,请求完成的时间就越长,由于以上的这些限制, cookie 只适合保存服务器必要的信息.
Web Storage
Web Storage 的目的是为了解决客户端需要使用 cookie 存储一些不需要频繁发送回服务器的数据.
sessionStorage
sessionStorage
的特点:
- 只存储会话数据,即数据只会存储到当前设置存储的 tab 页面被关闭或者是浏览器关闭
- 只能存储字符串类型数据,不能存储结构化数据
- 存储的数据不受页面刷新影响,可在浏览器崩溃并重启后恢复
- 大多数浏览器限制 1个源 只能存储 5MB
localStorage
localStorage
的特点:
- 存储的数据会一直保留,直到通过 JavaScript 删除或者用户清除浏览器缓存
- 只能存储字符串类型数据,不能存储结构化数据
- 存储的数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览器而丢失
- 大多数浏览器限制 1个源 只能存储 5MB
IndexDB
IndexedDB(Indexed Database API) ,是浏览器中 存储结构化数据 的一个方案,用于代替目前已废弃的 Web SQL Database API.
其中 API 的设计基本上是 异步的,大多数操作以请求的形式 异步执行,产生成功的结果或错误都有对应的 onerror
和 onsuccess
事件处理程序来确定输出.
IndexedDB
的特点:
- 能够存储结构化数据
- IndexedDB 数据库是与 页面源 绑定的,不能跨域共享
- 大小限制
- Chrome 限制是每个源 5MB
- Firefox 限制是每个源 50MB
- Firefox(移动版) 限制每个源 5MB ,若超出则请求用户许可
从以上可以看出 IndexedDB 的限制其实 Web Storage 一样.
页面源(源)= 协议 + 域 + 端口
IndexedDB
数据库
IndexedDB 数据库是类似于 MySQL 或 Web SQL Database 的数据库,它使用对象存储数据而不是使用表格,并且属于 NoSQL 的风格.
建立数据库连接 —— indexedDB.open()
indexedDB.open(name, version)
其中 name 为数据库名称,version 为数据库版本,且版本只能为整数
通过indexedDB.open(name, version)
使用数据库:
- 如果给定名称的数据库 已存在,则会发送一个 打开请求
- 如果 不存在,则会发送 创建 并 打开 这个数据库的请求
- 异步执行,返回 IDBRequest 实例,需要监听
onerror
和onsuccess
事件
如下将创建一个名为 test 的数据库:
let db, request, version = 1;
request = indexedDB.open("test", version);
request.onerror = (event) => alert(`Failed to open: ${event.target.errorCode}`);
request.onsuccess = (event) => {db = event.target.result;
};
对象存储 —— request.onupgradeneeded()
数据库 版本发生变化 或 创建新数据库时 就会触发 onupgradeneeded
事件,此时需要指定或换修改 数据库模式 —— 包括数据库中的 对象存储 和对象 存储结构
如下将在 test 数据库下创建名为 productions 的对象存储模式
// 需要存储的对象
let production = {id: "20220128",name: "产品xxx",
}// 指定对象存储的模式
request.onupgradeneeded = (event) => {const db = event.target.result;// 若存在就的存储结构则删除(可选)if (db.objectStoreNames.contains("productions")) {db.deleteObjectStore("productions");}// 创建对象存储,keyPath 表示应该用作键存储对象的属性名db.createObjectStore("productions", {keyPath: "id"});
};
事务 —— db.transaction()
IndexedDB 是基于事务的
创建对象存储之后,剩下的所有操作都是通过事务完成的,即若要 读写数据,都要通过事务 把所有修改操作进行组织.
创建事务
通过 let transaction = db.transaction()
方法来创建事务:
db.transaction()
— 不指定参数 则对数据库中所有 对象存储 有 只读 权限db.transaction("productions")
— 只加载特定对象存储的事务,只读 权限db.transaction(["productions","xxx"], "readwrite")
—— 加载多个对象存储的事务,且权限为 读写
transaction.onerror = (event) => {// 整个事务被取消
};
transaction.oncomplete = (event) => {// 整个事务成功完成
};
获取指定对象存储 —— transaction.objectStore()
通过 let store = transaction.objectStore("productions")
获取 productions 的对象存储,得到了对应的 对象存储 后,就可以使用以下方法:
store.get()
— 获取数据store.getAll()
— 获取所有数据store.add()
— 添加数据store.put()
— 更新数据store.delete()
— 删除数据store.clear()
— 清除数据- …
写入对象
通过对象存储的引用 store
使用 store.add()
和 store.put()
往对象存储中写入数据,它们都只接收一个要存储的对象作为参数,前提是这个对象必须包含存储对象中的 keyPath
属性对应的属性字段.
对象存储中已存在 同名的键 时:
store.add()
导致错误store.put()
重写该对象
// productions 是一个产品数组
let request, requests = [];
for (let product of productions) {request = store.add(product);request.onerror = () => {// 处理错误};request.onsuccess = () => {// 处理成功};requests.push(request);
}
查询数据
键查询 —— store.get()
键指的就是 onupgradeneeded
中指定的 keyPath
属性所对应的值,比如在上面我们指定了 { keyPath: 'id' }
,那么在使用键查询时,就必须要使用已存储数据对应属性的对应值.
假设需要获取如上图中的数据:
let storeName = "productions"
// 先创建事务,通过事务获取指定的存储对象
let store = db.transaction(storeName, 'readwrite').objectStore(storeName)
// 通过存储对象的 get 方法以及对应的 keyPath 键的 id 属性值获取数据
let request = store.get(1643527674720);
request.onsuccess = (event) => {console.log(event.target.result);
}
request.onerror = (event) => {console.log(`query_key error with code:${event.target.errorCode}`);
}
索引查询 —— store.index(key).get(value)
对某些数据集可能需要为对象存储指定多个键,以便于后续查询数据时可以通过不同的键去获取,需要创建存储对象时使用 store.createIndex()
去创建索引,即 onupgradeneeded
事件处理程序中,如下创建了一个名为 price
的索引,后续查询数据时就可以使用这个所以名和对应的值进行查找
创建索引
request.onupgradeneeded = (event) => {db = event.target.resultdb.createObjectStore("productions", {keyPath: "id"}).createIndex("price", "price", {unique: false});
};
使用索引
let storeName = "productions"let store = db.transaction(storeName, 'readwrite').objectStore(storeName)let request = store.index('price').get('72.57');request.onsuccess = (event) => {console.log(event.target.result);}request.onerror = (event) => {console.log(`query_index error with code:${event.target.errorCode}`);}
游标查询 —— store.openCursor() + cursor.continue()
从上面可以知道,使用事务通过一个已知键或者索引都可以取得一条记录,但如果想取得多条数据,那就需要在事务中创建一个游标.
let cursorRequest = store.openCursor();
cursorRequest.onsuccess = (event) => {let cursor = event.target.result;// 当游标 cursor 值不存在时,代表游标查找结束或出现异常,所以需要提前判断if (!cursor) return;console.log(cursor.value);// 在本次游标指向中,通过显示调用 continue 方法就可以将游标指向下一个数据cursor.continue();
}
cursorRequest.onerror = (event) => {console.log(`query_cursor error with code:${event.target.errorCode}`);
}
范围查找 —— index(索引) + cursor(游标) + IDBKeyRange(键范围)
IDBKeyRange(键范围)
IDBKeyRange.only(1643607832742)
得到对应键值的记录IDBKeyRange.lowerBound(1643607832742, false)
得到对应键值的下限(开始位置),第二个参数:false
—— 从1643607832742
记录开始,直到最后true
—— 从1643607832742
的下一条记录开始,直到最后
IDBKeyRange.upperBound(1643607832742, false)
得到对应键值的上限(结束位置),第二个参数:false
—— 从头开始,到1643607832742
记录为止true
—— 从头开始,到1643607832742
的前一条记录为止
IDBKeyRange.bound()
—— 同时指定上下限,接收四个参数:下限键、上限键、 可选布尔值 表示是否 跳过下限 、可选布尔值 是否 跳过上限
使用这样的查找方式更加理想,因为这样的方式可以实现根据指定索引在指定范围内开始游标查询,实现比较合理的范围查找.
let request = store.index('price').openCursor(IDBKeyRange.only(1643607832742));
request.onsuccess = (event) => {let cursor = event.target.result;if (!cursor) return;console.log(cursor.value);cursor.continue();
}
request.onerror = (event) => {console.log(`query_idnex_cursor error with code:${event.target.errorCode}`);
}
实现分页查询
模拟数据
要实现查询的前提是得先有数据,这里就通过 setTimeout
模拟异步请求获取数据:
const getData = () => {setTimeout(() => {const list = []const now = Date.now()for (let index = 0; index < 100; index++) {let price = Math.random() * 100price = price < 10 ? price + 10 : pricelist.push({id: now + index,name: `产品_${index + 1}`,price: price.toFixed(2)})}data.value = list})
}
然后通过循环调用 add()
方法将数写入到数据库中
const inser = (data = []) => {let objectStore = db.transaction(storeName, 'readwrite').objectStore(storeName)data.forEach(item => objectStore.add({ ...item }))console.log('数据插入成功!')
}
分页效果
实现效果如下
主要代码
useIndexDB.js
let db = null
let storeName = 'test_db'
let lastRecords = []
let currSize = 0
let indexArr = ["name", "price"]const getStoreByTransaction = () => db.transaction(storeName, 'readwrite').objectStore(storeName)const query = (payload) => {return new Promise((resovle, reject) => {let records = []let { name, value, type, size = 10 } = payload || {}let objectStore = getStoreByTransaction()let requestif (type) {request = type === 'next'? objectStore.openCursor(IDBKeyRange.lowerBound(lastRecords[lastRecords.length - 1].id, type)): objectStore.openCursor(IDBKeyRange.upperBound(lastRecords[0].id, true), type)} else {request = name? objectStore.index(name).openCursor(IDBKeyRange.only(value), type): objectStore.openCursor()}lastRecords = []request.onsuccess = (event) => {let cursor = event.target.resultif (!cursor || currSize >= size) {currSize = 0resovle(records)return}let lastItem = cursor.valuelastRecords.push(lastItem)records.push(lastItem)currSize++if (!name) {cursor.continue()} else {currSize = 0resovle(records)}}request.onerror = (event) => {reject(`query_idnex_cursor error with code:${event.target.errorCode}`)}})
}const inser = (data = []) => {let objectStore = db.transaction(storeName, 'readwrite').objectStore(storeName)data.forEach(item => objectStore.add({ ...item }))console.log('数据插入成功!')
}const open = (payload) => {payload = payload || { name: 'test', version: 1, storeName: "productions" }return new Promise((resovle, reject) => {let request = indexedDB.open(payload.name, payload.version)storeName = payload.storeNamerequest.onsuccess = (event) => {db = event.target.resultresovle({query,inser,getStoreByTransaction,db})console.log(`Database connection successfully established`)}request.onerror = (event) => {reject(`Failed to open: ${event.target.errorCode}`)}request.onupgradeneeded = (event) => {db = event.target.resultif (db.objectStoreNames.contains("productions")) {db.deleteObjectStore("productions")}let objectStore = db.createObjectStore("productions", {keyPath: "id"})indexArr.forEach((name) => {objectStore.createIndex(name, name, {unique: false})})}})
}export default open
App.vue
<script setup lang="ts">
import { reactive, ref, onMounted, computed } from "vue"
import openDB from "./useIndexDB.js";const data = ref([])
const queryResult = ref([])
const formItem = reactive({ price: '', name: '' })
let dbObj = nullonMounted(() => {openDB().then(({ query, inser }) => {dbObj = { query, inser }})
})const getData = () => {setTimeout(() => {const list = []const now = Date.now()for (let index = 0; index < 100; index++) {let price = Math.random() * 100price = price < 10 ? price + 10 : pricelist.push({id: now + index,name: `产品_${index + 1}`,price: price.toFixed(2)})}data.value = listconsole.log("数据获取成功!", data.value);})
}const insertData = () => {dbObj.inser(data.value)
}const payload = computed(() => {let obj = { value: formItem.name }if (formItem.name) {obj.name = 'name'obj.value = formItem.name}if (formItem.price) {obj.name = 'price'obj.value = formItem.price}return obj
})const loadPage = (type) => {dbObj.query({ type }).then((result) => {console.log(result)queryResult.value = result.sort((a,b)=> a.id - b.id)})
}const queryData = () => {dbObj.query({ ...payload.value }).then((result) => {console.log(result)queryResult.value = result.sort()})
}</script><template><div class="btn-box"><button @click="getData">获取数据</button><button @click="insertData">插入数据</button></div><div class="box"><label for="name">名称:<input id="name" v-model="formItem.name" type="text" /></label><label for="price">价格:<input id="price" v-model="formItem.price" type="text" /></label><button class="btn" @click="queryData">查询</button></div><ul class="list" v-show="queryResult.length"><li v-for="item in queryResult" :key="item.id">{{item.name}} —— {{ item.price}}</li><div><button @click="loadPage('prev')">上一页</button><button @click="loadPage('next')">下一页</button></div></ul>
</template><style>
#app {font-family: Avenir, Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-align: center;color: #2c3e50;margin-top: 60px;transition: all 0.5s ease;
}
.box label {margin: 0 10px;
}
button {margin: 10px;
}
.list{width: 400px;margin: 10px auto;
}
</style>
相关文章:

客户端存储 — IndexedDB 实现分页查询
前言 相信 IndexedDB 大家都有过了解,但是不一定每个人都有过实践,并且其中涉及到事务、游标等概念,会导致在初次使用时会有些不适应,那么本文会通过 IndexedDB 实现分页查询的形式进行实践,在开始之前,可…...

logback 如何将日志输出到文件
如何作 将日志输出到文件需要使用 RollingFileAppender,该 Appender 必须定义 rollingPolicy ,另外 rollingPollicy 下必须定义 fileNamePattern 和 encoder <appender name"fileAppender" class"ch.qos.logback.core.rolling.Rollin…...
Files.newBufferedReader和Files.readAllLines
在Java中,Files.newBufferedReader 和 Files.readAllLines 都是用于从文件中读取数据的工具方法,但它们的使用场景和功能有所不同。下面我将详细解释这两个方法的含义、用途、区别、优缺点以及各自的使用场景。 1. Files.newBufferedReader 含义和用途…...

MySQL 数据库备份与恢复全攻略
MySQL 数据库备份与恢复全攻略 引言 在现代应用中,数据库是核心组件之一。无论是个人项目还是企业级应用,数据的安全性和完整性都至关重要。为了防止数据丢失、损坏或意外删除,定期备份数据库是必不可少的。本文将详细介绍 MySQL 数据库的备…...

Appium中的api(一)
目录 1.基础python代码准备 1--参数的一些说明 2--python内所要编写的代码 解释 2.如何获取包名和界面名 1-api 2-完整代码 代码解释 3.如何关闭驱动连接 4.安装卸载app 1--卸载 2--安装 5.判断app是否安装 6.将应用放到后台在切换为前台的时间 7.UIAutomatorViewer的使用 1--找…...

【AI辅助设计】没错!训练FLUX LoRA就这么简单!
前言 得益于开源社区的力量,在各位大佬的努力下,现在16G VRAM的家用电脑也可以训练FLUX的LoRA了 👏。 今天我使用fluxgym这个方法,训练LoRA,并记录过程。 篇幅有限,这里就不一一展示了,有需要的…...
Mac 下安装FastDFS
首先我们需要下载相对应的安装包: libfastcommonFastDFS 下载完成后我们先将其解压到桌面。 1.安装libfastcommon 我们进入到libfastcommon-master目录中执行./make.sh和sudo ./make.sh install,具体代码如下: 2.安装FastDFS 同安装libfa…...
人工智能的未来:重塑生活与工作的变革者
随着人工智能(AI)技术的快速发展,我们正处于一个前所未有的变革时代。AI不仅在医疗、企业运营和日常生活中发挥着重要作用,而且正在重新定义我们的生活和工作方式。本文将探讨人工智能技术的应用前景以及它如何改变我们的生活和工…...

【微服务】Java 对接飞书多维表格使用详解
目录 一、前言 二、前置操作 2.1 开通企业飞书账户 2.2 确保账户具备多维表操作权限 2.3 创建一张测试用的多维表 2.4 获取飞书开放平台文档 2.5 获取Java SDK 三、应用App相关操作 3.1 创建应用过程 3.2 应用发布过程 3.3 应用添加操作权限 四、多维表应用授权操作…...

学习threejs,使用粒子实现下雪特效
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️THREE.Points简介1.11 ☘️…...
unity3d——Time
在Unity3D中,Time类是一个非常重要的工具类,它提供了一系列与时间相关的属性和方法,帮助开发者在游戏中实现各种时间相关的操作。以下是一些Time类常用的方法及其示例: 一、常用属性 Time.time 含义:表示从游戏开始到…...

天地图实现海量聚合marker--uniapp后端详细实现
本文章详细的讲解了前后端代码来 实现uniapp天地图功能的实现 以及 后端海量数据的聚合查询 和网格算法实现思路。 并对当数据量增加和用户频繁请求接口时可能导致服务器负载过高做了前后端优化。 前端uniapp: 实现了天地图的行政区划边界/地图切换/比例尺/海量数…...

Bug | 项目中数据库查询问题
问题描述 理论上,点击查询后,表头应当显示中文。而不是上面的在数据库中的表头【如上图示】 正常点击查询后,如果没有输入值,应当是查询所有的信息。 原因分析: 这里是直接使用SELECT * 导致的。例如: S…...

C++入门基础知识129—【关于C 库函数 - time()】
成长路上不孤单😊😊😊😊😊😊 【14后😊///C爱好者😊///持续分享所学😊///如有需要欢迎收藏转发///😊】 今日分享关于C 库函数 - time()的相关内容࿰…...

大文件秒传,分片上传,断点续传
大文件分片上传 一 功能描述 1.文件通过web端分片多线程上传到服务端,然后web端发起分片合并,完成大文件分片上传功能 2.上传过的大文件,实现秒传 3.上传过程中,服务异常退出,实现断点续传 二 流程图 三 代码运行…...

多生境扩增子探秘:深度溯源与多样性解析
分析微生物组数据的组成结构的一个主要挑战是确定其潜在来源。在微生物来源分析中,随机森林、SourceTracker和FEAST都有较广泛应用。今天,小编就带大家看一篇发表在《iMeta》的文章,使用溯源技术追踪微生物的来源与去向,揭示生物在…...

Selenium4自动化测试常用函数总结,各种场景操作实战
🍅 点击文末小卡片 ,免费获取软件测试全套资料,资料在手,涨薪更快 seleninum作为自动化测试的工具,自然是提供了很多自动化操作的函数,下面列举下比较常用的函数,更多可见官方文档:…...

图像生成新范式:智源推出全能视觉生成模型 OmniGen
大型语言模型(LLM)的出现统一了语言生成任务,并彻底改变了人机交互。然而,在图像生成领域,能够在单一框架内处理各种任务的统一模型在很大程度上仍未得到探索。近日,智源推出了新的扩散模型架构 OmniGen&am…...
实现RPC接口的demo记录
1.Thrift RPC 接口实现 Demo Service public class DemoServiceImpl implements DemoService.Iface {private static final Logger logger LoggerFactory.getLogger(DemoServiceImpl.class);Overridepublic String sayHello(Context context, String msg) throws TException …...
Python期末题目 | 期末练习题【概念题+代码】
一、前言 Python 是一门功能强大且易于学习的编程语言,在高校中被广泛用作教学语言。Python 的期末考试通常会包含基础知识和编程实践,以考察学生的理解与应用能力。本文整理了一套 Python 期末练习题,包括选择题、填空题、判断题和代码题。…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...

用docker来安装部署freeswitch记录
今天刚才测试一个callcenter的项目,所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...

GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...

Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...

AI语音助手的Python实现
引言 语音助手(如小爱同学、Siri)通过语音识别、自然语言处理(NLP)和语音合成技术,为用户提供直观、高效的交互体验。随着人工智能的普及,Python开发者可以利用开源库和AI模型,快速构建自定义语音助手。本文由浅入深,详细介绍如何使用Python开发AI语音助手,涵盖基础功…...
基于鸿蒙(HarmonyOS5)的打车小程序
1. 开发环境准备 安装DevEco Studio (鸿蒙官方IDE)配置HarmonyOS SDK申请开发者账号和必要的API密钥 2. 项目结构设计 ├── entry │ ├── src │ │ ├── main │ │ │ ├── ets │ │ │ │ ├── pages │ │ │ │ │ ├── H…...

从物理机到云原生:全面解析计算虚拟化技术的演进与应用
前言:我的虚拟化技术探索之旅 我最早接触"虚拟机"的概念是从Java开始的——JVM(Java Virtual Machine)让"一次编写,到处运行"成为可能。这个软件层面的虚拟化让我着迷,但直到后来接触VMware和Doc…...

算法打卡第18天
从中序与后序遍历序列构造二叉树 (力扣106题) 给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。 示例 1: 输入:inorder [9,3,15,20,7…...

车载诊断架构 --- ZEVonUDS(J1979-3)简介第一篇
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...