前言
相信 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.result
db.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() * 100
price = price < 10 ? price + 10 : price
list.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 request
if (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.result
if (!cursor || currSize >= size) {
currSize = 0
resovle(records)
return
}
let lastItem = cursor.value
lastRecords.push(lastItem)
records.push(lastItem)
currSize++
if (!name) {
cursor.continue()
} else {
currSize = 0
resovle(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.storeName
request.onsuccess = (event) => {
db = event.target.result
resovle({
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.result
if (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 = null
onMounted(() => {
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() * 100
price = price < 10 ? price + 10 : price
list.push({
id: now + index,
name: `产品_${index + 1}`,
price: price.toFixed(2)
})
}
data.value = list
console.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,存储,分页,db,price,request,let,event,客户端
From: https://blog.csdn.net/weixin_43822185/article/details/143321010