IndexedDB前言
IndexedDB
是浏览器提供的一种本地数据库,用于在用户浏览器中存储大量的结构化数据,包括文件二进制数据。它是一个异步的,基于事务的noSQL
数据库,适用于需要脱机访问,数据缓存,大型数据集存储等场景。
非关系型数据库(NoSql)
,indexedDB
是非关系型数据库,主要以键值对的形式存储数据。
持久化存储,localStorage
,cookie
,sessionStorage
等方式i存储的数据,清除浏览器缓存之后,这些数据都会被清除掉,而实用indexedDB存
储的数据不会,除非手动删除。
indexedDB
操作时不会锁死浏览器,用户依然可以进行其他的操作,与localStorage
对比很鲜明,localStorage
是同步的。
indexedDB
支持事务,这意味着一些列的操作和步骤之中,只要有一步失败了,整个事务就会取消,数据库回滚到事务发生之前的状态,这个MySQL
等数据库的事务类似。
IndexedDB 主要特点
特性 | 说明 |
---|---|
持久化存储 | 数据保存在用户浏览器中,不会因为刷新或关闭浏览器而丢失。 |
结构化数据 | 支持存储对象嵌套对象,Blob ,ArrayBuffer 等。 |
异步操作 | 使用事件或者promise 避免阻塞主线程。 |
基于对象存储 | 类似数据库中的表,成为object store ,每个对象存储类似于一张表。 |
索引支持 | 可以为字段创建索引,加快数据查询。 |
事务机制 | 操作数据的时候需要通过事务进行,保证数据的一致性和完整性。 |
IndexedDB与其他浏览器存储区别
特性 | cookie | sessionStorage | localStorage | indexedDB |
---|---|---|---|---|
容量大小 | 每个域名4KB | 5MB | 5~10MB | 根据本机自身存储容量,通常数百MB-GB |
生命周期 | 可设置事件 | 页面绘画关闭即清除 | 永久除非主动清除 | 永久除非主动清除 |
作用范围 | 同源所有路径共享 | 当前标签页/窗口 | 同源共享 | 同源共享 |
是否随请求发送 | 是(每次HTTP 请求都会附带) | 否 | 否 | 否 |
数据类型 | 字符串(需要手动编码) | 字符串(需要手动编码) | 字符串(需要手动编码) | 可以存储对象,二进制,结构化数据 |
同步/异步 | 同步(阻塞) | 同步(阻塞) | 同步(阻塞) | 异步(阻塞) |
可编程接口 | 简单API | 简单API ,键值对 | 简单API ,键值对 | 强大API ,支持事务/索引等 |
适合用途 | 跨请求数据,如登录状态 | 临时数据,如表单临时UI状态 | 持久数据,如用户设置缓存 | 大量结构化数据,如离线应用,本地数据库 |
IndexedDB实用场景
- 离线应用
- 大量数据本地缓存,比如地图和表格数据
- 存储用户设置,表单草稿
- 替代
localStorage
的方案
IndexedDB基本概念
仓库objectStore
indexedDB
没有表的概念,它只有仓库store
的概念,可以把仓库理解为表即可,即一个store是一张表。
索引index
在关系型数据库当中也有索引的概念,我们可以给对应的表字段添加索引,以便加快查找速率,在indexedDB
中同样有索引,我们可以在创建store
的时候创建索引,在后续对store
进行查询的时候通过索引来筛选,给某个字段添加索引后,在后续插入数据的过程中,索引字段不能为空。
游标cursor
游标是indexedDB
数据库新的概念,大家可以把游标想象为一个指针,比如我们要查询满足某一条件所有数据时,就需要用到游标,让游标一行一行的往下走,游标走到的地方便会返回一行数据,此时我们便可对此行数据进行判断是否满足条件。
事务
indexedDB
支持事务,即对数据库进行操作的时候,只要失败了,都会回滚到最初始的状态,确保数据的一致性。
indexedDB操作
indexedDB
所有针对仓库的操作都是基于事务的。
插入操作
indexedDB
插入数据需要通过事务来进行操作,插入的方法也很简单,利用indexedDB
提供的add
方法即可,但是要注意的是,插入的数据是一个对象,而且还包含了我们声明的索引键值对。
插入函数封装
/**
* @param {*} db 在创建或链接数据库时候,返回的db实例
* @param {*} storeName 仓库名称,在创建或者连接数据库时候就已经创建好了
* @param {*} data 需要插入的数据,通常是一个对象
*/
function addData(db, storeName, data) {
let request = db.transaction([storeName], "readwrite") // 创建一个事务
.objectStore(storeName) // 获取仓库
.add(data); // 添加数据
request.onsuccess = function (e) {
console.log("数据添加成功");
};
request.onerror = function (e) {
console.log("数据添加失败");
};
}
插入函数调用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IndexedDB数据库</title>
<script src="./indexedDB.js"></script>
</head>
<body></body>
<script>
let db;
// 创建数据库
openDB("studentDB", 1).then((db) => {
db = db;
console.log(db);
// 插入数据
addData(db, "user", {
uuid: new Date(),
name: "张三",
age: parseInt(Math.random() * 100),
});
});
</script>
</html>
插入效果
通过主键读取数据
封装通过主键查询方法
// 通过主键查询数据
function getDataByKey(db, storeName, key) {
var transaction = db.transaction([storeName]); // 声明事务
var objectStore = transaction.objectStore(storeName); // 仓库对象
var request = objectStore.get(key); // 通过主键获取数据
request.onsuccess = function (e) {
console.log("数据获取成功", request.result);
return request.result;
};
request.onerror = function (e) {
console.log("数据查询失败");
};
}
也可以通过下面getAll()
获取所有数据
var request = objectStore.getAll(); // 通过主键获取数据
调用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IndexedDB数据库</title>
<script src="./indexedDB.js"></script>
</head>
<body></body>
<script>
let db;
// 创建数据库
openDB("studentDB", 1).then((db) => {
db = db;
console.log(db);
// 插入数据
addData(db, "user", {
uuid: parseInt(Math.random() * 100),
name: "张三",
age: parseInt(Math.random() * 100),
});
// 通过主键查询数据
getDataByKey(db, "user", 61);
});
</script>
</html>
查询效果
通过游标读取数据
封装游标查询方法
// 通过游标查询数据
function cursorGetData(db, storeName) {
let list = [];
var store = db
.transaction(storeName, "readwrite") // 事务
.objectStore(storeName) // 仓库对象
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);
return list;
}
};
}
调用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IndexedDB数据库</title>
<script src="./indexedDB.js"></script>
</head>
<body></body>
<script>
let db;
// 创建数据库
openDB("studentDB", 1).then((db) => {
db = db;
console.log(db);
// 插入数据
addData(db, "user", {
uuid: parseInt(Math.random() * 100),
name: "张三",
age: parseInt(Math.random() * 100),
});
// 通过主键查询数据
getDataByKey(db, "user", 61);
// 通过游标查询数据
cursorGetData(db, "user");
});
</script>
</html>
查询效果
通过索引来查询数据
封装索引查询数据方法
// 通过索引查询数据,只查询第一条满足的数据
function getDataByIndex(db, storeName, indexName, indexValue) {
var store = db
.transaction(storeName, "readwrite")
.objectStore(storeName);
var request = store.index(indexName).get(indexValue);
request.onsuccess = function (e) {
var result = e.target.result;
console.log("通过索引查询数据 ", result);
};
request.onerror = function (e) {
console.log("通过索引查询数据失败 ", e.target.result);
};
}
调用方法及查询结果
通过索引和游标查询数据
通过索引查询数据可以看出,得到的数据只有一个,因为通过索引查到第一个数据就会停止,不会继续往下查,如果想查询所有索引值满足条件的就需要用到游标进行查询。
方法 | 说明 | 参数说明 |
---|---|---|
IDBKeyRange.only(value) | 精确匹配 | value需要精确匹配的值 |
IDBKeyRange.lowerBound(lower, open) | 指定下限范围 | open 为false 包含下限值>= ,open 为true 不包含下限值> |
IDBKeyRange.upperBound(upper, open) | 指定上限范围 | open 为false 包含上限值<= ,open 为true 不包含上限值< |
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen) | 指定上下限范围 | lowerOpen 和upperOpen 分别表示是否排除下限和上限的值。 |
openCursor()
方法用于遍历对象存储或索引中的记录。结合上述键范围方法,可以实现灵活的数据查询。以下是一个使用 openCursor()
封装索引和游标查询方法
// 通过索引和游标查询数据
function cursorGetDataByIndex(db, storeName, indexName, indexValue) {
let list = []
var store = db.transaction(storeName, "readwrite").objectStore(storeName); // 获取对象仓库
var request = store.index(indexName).openCursor(IDBKeyRange.only(indexValue)); // 指针对象
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
// 必须检查
list.push(cursor.value)
cursor.continue(); // 遍历了存储对象中的所有内容
} else {
console.log("通过索引和游标查询数据成功 ", list);
}
};
request.onerror = function (e) {
console.log("通过索引和游标查询数据失败 ", e.target.result);
};
}
游标索引查询效果
通过索引和游标分页查询
indexedDB
分页查询不像MySQL
分页查询那么简单,没有提供现成的API
如limit
等,所以需要自己实现分页,实现过程中用到了indexedDB
的一个advanceAPI
,该API
可以让游标跳过多少条开始查询,加入分也是每页10
条数据,现在需要查询第二页,那我们就需要跳过前面十条数据,从11
条数据开始查询,知道计数器等于10
,就关闭游标结束查询。
封装索引游标分页查询方法
/**
* 通过索引和游标分页查询数据
* @param {*} db // 数据库
* @param {*} storeName // 存储对象名称
* @param {*} query // 查询条件
* @param {*} callback // 回调函数
*/
function queryByConditionsAndPage(db, storeName, query, callback) {
const { pageSize = 10, currentPage = 1, ...filters } = query;
const transaction = db.transaction([storeName], "readonly"); // 创建事务
const store = transaction.objectStore(storeName); // 获取存储对象
const request = store.openCursor(); // 打开游标
const results = []; // 结果集
let total = 0; // 总数
let advanced = false; // 是否已跳过指定条数
const startPosition = pageSize * (currentPage - 1); // 开始位置
request.onsuccess = function (event) { // 获取成功
const cursor = event.target.result;
if (!cursor) {
// 查询结束,返回结果
callback({ total, data: results });
return;
}
const record = cursor.value; // 当前记录
let match = true; // 是否匹配
// 检查每个过滤条件
for (const key in filters) {
if (filters.hasOwnProperty(key)) {
if (record[key] !== filters[key]) {
match = false;
break;
}
}
}
if (match) { // 匹配
total++;
if (total > startPosition && results.length < pageSize) {
results.push(record);
}
}
cursor.continue(); // 继续遍历下一条记录
};
request.onerror = function (event) {
console.error("查询失败:", event.target.error);
callback({ total: 0, data: [] });
};
}
调用索引分页查询方法
// 通过索引和游标分页查询
const query = {
pageSize: 10,
currentPage: 1,
name: "张三",
};
queryByConditionsAndPage(db, "user", query, function (result) {
console.log("查询数据", result);
});
分页查询效果
更新数据
封装更新数据方法
// 更新数据
function updateDB(db, storeName, data) {
var request = db.transaction([storeName], "readwrite")
.objectStore(storeName)
.put(data);
request.onsuccess = function (event) {
console.log("数据更新成功");
};
request.onerror = function (event) {
console.error("数据更新失败:");
};
}
调用更新数据方法
let update = {
uuid: 7,
name: "吴又可",
age: 24,
};
updateDB(db, "user", update);
注意:其中如果uuid
未匹配到,则是添加新的数据。
更新数据效果
通过主键删除数据
封装调用删除方法
// 通过主键删除数据
function deleteDB(db, storeName, id) {
var request = db.transaction([storeName], "readwrite")
.objectStore(storeName)
.delete(id);
request.onsuccess = function (event) {
console.log("数据删除成功");
};
request.onerror = function (event) {
console.error("数据删除失败:");
};
}
调用:
deleteDB(db, "user", 7);
删除前后的对比效果
通过索引和游标删除指定数据
封装调用
// 通过游标和索引删除数据
function deleteRecordsByConditions(db, storeName, conditions, callback) {
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.openCursor();
let deleteCount = 0;
request.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
const record = cursor.value;
let match = true;
// 检查每个条件
for (const key in conditions) {
if (conditions.hasOwnProperty(key)) {
if (record[key] !== conditions[key]) {
match = false;
break;
}
}
}
if (match) {
const deleteRequest = cursor.delete();
deleteRequest.onsuccess = function () {
deleteCount++;
};
deleteRequest.onerror = function (event) {
console.error('删除记录失败:', event.target.error);
};
}
cursor.continue();
} else {
// 所有记录已遍历完成
callback(deleteCount);
}
};
request.onerror = function (event) {
console.error('游标打开失败:', event.target.error);
callback(0);
};
}
调用:
const conditions = {
name: "张三",
};
deleteRecordsByConditions(
db,
"user",
conditions,
function (deletedCount) {
console.log(`共删除了 ${deletedCount} 条记录`);
}
);
根据条件删除所有满足条件数据
关闭数据库
封装方法
// 关闭数据库
function closeDB(db) {
return new Promise((resolve) => {
db.close(); // 实际是同步的
console.log("关闭数据库成功");
resolve(); // 手动返回 Promise 让调用端等待
});
}
调用方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IndexedDB数据库</title>
<script src="./indexedDB.js"></script>
</head>
<body></body>
<script>
// 封装为异步函数,统一 await 调用链
async function run() {
const db = await openDB("studentDB", 1);
await addData(db, "user", {
uuid: parseInt(Math.random() * 100),
name: "张三",
age: parseInt(Math.random() * 10),
});
await new Promise((resolve) => {
getDataByKey(db, "user", 61);
resolve(); // 非异步返回的兼容处理
});
await new Promise((resolve) => {
cursorGetData(db, "user");
resolve();
});
await new Promise((resolve) => {
getDataByIndex(db, "user", "age", 9);
resolve();
});
await new Promise((resolve) => {
cursorGetDataByIndex(db, "user", "age", 9);
resolve();
});
const query = {
pageSize: 10,
currentPage: 1,
name: "张三",
};
await new Promise((resolve) => {
queryByConditionsAndPage(db, "user", query, function (result) {
console.log("查询数据", result);
resolve();
});
});
let update = {
uuid: 7,
name: "吴又可",
age: 24,
};
await new Promise((resolve) => {
updateDB(db, "user", update);
resolve();
});
const conditions = {
name: "张三",
};
await new Promise((resolve) => {
deleteRecordsByConditions(
db,
"user",
conditions,
function (deletedCount) {
console.log(`共删除了 ${deletedCount} 条记录`);
resolve();
}
);
});
await closeDB(db); // 最后执行
}
run();
</script>
</html>