IndexDB 数据库使用
·
1、是什么
IndexedDB 是浏览器内置的 NoSQL 数据库,用于在客户端存储大量结构化数据(如 JSON 格式数据),支持离线访问、事务操作和索引查询,是前端实现本地数据持久化的核心方案之一(适用于 PWA、离线应用、大型表单缓存等场景)
2、核心概念
| IndexedDB 概念 | 类比传统数据库 | 说明 |
|---|---|---|
| 数据库(Database) | 数据库实例 | 一个浏览器中可创建多个数据库,每个数据库有唯一名称,且版本号递增(版本更新触发升级事件)。 |
| 对象仓库(Object Store) | 数据表(Table) | 数据库中存储数据的 “表”,每个对象仓库存储一类结构化数据(如 “用户表”“商品表”),需指定主键(Primary Key)。 |
| 事务(Transaction) | 事务 | 操作数据的最小单元,支持 readwrite(读写)和 readonly(只读)模式,所有数据操作必须在事务中执行,且具备原子性(要么全成功,要么全失败)。 |
| 索引(Index) | 索引 | 为对象仓库的非主键字段创建索引,加速查询(如为 “用户表” 的 “手机号” 字段建索引,避免全表扫描)。 |
| 游标(Cursor) | 游标 | 遍历对象仓库中数据的工具,支持按条件筛选、排序,适用于批量查询或分页加载。 |
3、使用
1、封装工具类
// src/utils/indexedDBUtil.js
class IndexedDBUtil {
constructor(dbName, version, storeConfigs) {
this.dbName = dbName; // 数据库名称
this.version = version; // 数据库版本(必须为正整数)
this.storeConfigs = storeConfigs; // 对象仓库配置(数组,支持多表)
this.db = null; // 数据库实例(连接成功后赋值)
}
/**
* 1. 打开数据库(核心方法:创建/升级数据库、创建对象仓库和索引)
*/
openDB() {
return new Promise((resolve, reject) => {
// 1. 调用浏览器 indexedDB API 打开数据库
const request = window.indexedDB.open(this.dbName, this.version);
// 2. 数据库首次创建或版本升级时触发(仅执行一次)
request.onupgradeneeded = (event) => {
this.db = event.target.result;
console.log(`数据库 ${this.dbName} 版本升级至 ${this.version}`);
// 3. 遍历创建对象仓库(按配置)
this.storeConfigs.forEach(({ storeName, keyPath, indexes }) => {
// 若对象仓库已存在,先删除(避免版本升级时冲突,实际项目可按需调整)
if (this.db.objectStoreNames.contains(storeName)) {
this.db.deleteObjectStore(storeName);
}
// 创建对象仓库:keyPath 为主键(如用户ID),autoIncrement 可选(自增主键)
const objectStore = this.db.createObjectStore(storeName, {
keyPath, // 主键字段(如 "id")
// autoIncrement: true, // 若主键自增,可添加此配置
});
// 4. 为对象仓库创建索引(若有)
if (indexes && indexes.length) {
indexes.forEach(({ indexName, indexPath, unique }) => {
// indexName:索引名;indexPath:索引字段;unique:是否唯一(如手机号唯一)
objectStore.createIndex(indexName, indexPath, { unique });
});
}
});
};
// 5. 数据库打开成功
request.onsuccess = (event) => {
this.db = event.target.result;
console.log(`数据库 ${this.dbName} 打开成功`);
resolve(this.db);
};
// 6. 数据库打开失败(如版本号降级、浏览器不支持)
request.onerror = (event) => {
reject(new Error(`数据库打开失败:${event.target.error.message}`));
};
});
}
/**
* 2. 获取事务和对象仓库(所有数据操作依赖此方法)
* @param {string} storeName - 对象仓库名称
* @param {string} mode - 事务模式:'readwrite'(读写)/ 'readonly'(只读)
*/
getStore(storeName, mode = 'readonly') {
if (!this.db) throw new Error('数据库未连接,请先调用 openDB()');
// 1. 创建事务(指定操作的对象仓库和模式)
const transaction = this.db.transaction(storeName, mode);
// 2. 获取对象仓库实例
const store = transaction.objectStore(storeName);
// 3. 监听事务失败事件
transaction.onerror = (event) => {
throw new Error(`事务失败:${event.target.error.message}`);
};
return store;
}
/**
* 3. 新增数据(Add:主键重复时会报错,适合新增)
* @param {string} storeName - 对象仓库名称
* @param {object} data - 新增数据(需包含主键,除非配置 autoIncrement)
*/
addData(storeName, data) {
const store = this.getStore(storeName, 'readwrite');
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve('数据新增成功');
request.onerror = (event) => reject(new Error(`新增失败:${event.target.error.message}`));
});
}
/**
* 4. 更新数据(Put:主键存在则更新,不存在则新增)
* @param {string} storeName - 对象仓库名称
* @param {object} data - 更新数据(必须包含主键)
*/
putData(storeName, data) {
const store = this.getStore(storeName, 'readwrite');
return new Promise((resolve, reject) => {
const request = store.put(data);
request.onsuccess = () => resolve('数据更新成功');
request.onerror = (event) => reject(new Error(`更新失败:${event.target.error.message}`));
});
}
/**
* 5. 根据主键查询数据(Get:快速查询单个数据)
* @param {string} storeName - 对象仓库名称
* @param {*} key - 主键值(如用户ID:1001)
*/
getDataByKey(storeName, key) {
const store = this.getStore(storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = (event) => resolve(event.target.result); // 无数据时返回 null
request.onerror = (event) => reject(new Error(`查询失败:${event.target.error.message}`));
});
}
/**
* 6. 根据索引查询数据(Index:按非主键字段查询,需先创建索引)
* @param {string} storeName - 对象仓库名称
* @param {string} indexName - 索引名称
* @param {*} value - 索引字段值(如手机号:13800138000)
*/
getDataByIndex(storeName, indexName, value) {
const store = this.getStore(storeName);
return new Promise((resolve, reject) => {
// 1. 获取索引实例
const index = store.index(indexName);
// 2. 根据索引值查询(get() 查单个,getAll() 查所有匹配)
const request = index.get(value);
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = (event) => reject(new Error(`索引查询失败:${event.target.error.message}`));
});
}
/**
* 7. 删除数据(Delete:根据主键删除)
* @param {string} storeName - 对象仓库名称
* @param {*} key - 主键值
*/
deleteData(storeName, key) {
const store = this.getStore(storeName, 'readwrite');
return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onsuccess = () => resolve('数据删除成功');
request.onerror = (event) => reject(new Error(`删除失败:${event.target.error.message}`));
});
}
/**
* 8. 关闭数据库
*/
closeDB() {
if (this.db) {
this.db.close();
this.db = null;
console.log(`数据库 ${this.dbName} 已关闭`);
}
}
/**
* 9. 删除数据库(谨慎使用!会删除所有对象仓库和数据)
*/
deleteDB() {
return new Promise((resolve, reject) => {
const request = window.indexedDB.deleteDatabase(this.dbName);
request.onsuccess = () => {
this.db = null;
resolve(`数据库 ${this.dbName} 已删除`);
};
request.onerror = (event) => reject(new Error(`删除数据库失败:${event.target.error.message}`));
});
}
/**
* 模糊查询数据(核心新增方法)
* @param {string} storeName - 对象仓库名称
* @param {string} field - 要查询的字段(如 'name'、'title')
* @param {string} keyword - 模糊查询关键词
* @param {RegExp} [regExp] - 可选:自定义正则表达式(优先级高于 keyword)
* @returns {Promise<Array>} 匹配的结果数组
*/
fuzzySearch(storeName, field, keyword, regExp) {
const store = this.getStore(storeName);
return new Promise((resolve, reject) => {
const result = [];
// 关键词去空格 + 转义特殊字符(增强鲁棒性)
const trimmedKeyword = (keyword + '').trim();
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = regExp || new RegExp(escapeRegExp(trimmedKeyword), 'i');
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const data = cursor.value;
// 允许数字/字符串类型的字段,其他类型(如 null/undefined)跳过
if (data[field] !== undefined && data[field] !== null) {
// 将字段值转为字符串(无论原类型是数字还是字符串)
const fieldValue = String(data[field]);
if (regex.test(fieldValue)) {
result.push(data);
}
} else {
console.log(`字段 ${field} 的值为 null/undefined,跳过`);
}
cursor.continue();
} else {
resolve(result);
}
};
request.onerror = (event) => {
reject(new Error(`模糊查询失败:${event.target.error.message}`));
};
});
}
/**
* 批量获取所有数据(辅助方法,用于演示)
* @param {string} storeName - 对象仓库名称
*/
getAllData(storeName) {
const store = this.getStore(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll(); // 获取所有数据(IndexedDB 2.0+ 支持)
request.onsuccess = (event) => resolve(event.target.result);
request.onerror = (event) => reject(new Error(`获取所有数据失败:${event.target.error.message}`));
});
}
/**
* 分页查询(按主键范围,适合主键自增的场景)
* @param {string} storeName - 对象仓库名称
* @param {number} pageNum - 页码(从 1 开始)
* @param {number} pageSize - 每页条数
* @param {IDBKeyRange} [keyRange] - 可选:主键范围(如只查询 ID 100~200 的数据)
* @returns {Promise<{data: Array, total: number}>} 分页结果(数据+总条数)
*/
async paginationQuery(storeName, pageNum = 1, pageSize = 10, keyRange) {
const store = this.getStore(storeName);
const result = { data: [], total: 0, pageNum, pageSize, totalPage: 0 };
try {
// 1. 先查询总条数(用于计算总页数)
result.total = await new Promise((resolve) => {
const countRequest = keyRange
? store.count(keyRange) // 按范围统计
: store.count(); // 统计所有数据
countRequest.onsuccess = () => resolve(countRequest.result);
});
// 2. 计算分页起始位置(跳过前 (pageNum-1)*pageSize 条数据)
const skip = (pageNum - 1) * pageSize;
if (skip >= result.total) {
// 若起始位置超出总条数,直接返回空数据
return result;
}
// 3. 打开游标,跳过前 skip 条,读取 pageSize 条数据
await new Promise((resolve) => {
let current = 0; // 当前已读取的条数
// 打开游标(可选:按主键范围过滤)
const request = store.openCursor(keyRange);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (current < skip) {
// 跳过前 skip 条数据
current++;
cursor.continue();
} else if (current < skip + pageSize) {
// 读取当前页数据(未超过 pageSize)
result.data.push(cursor.value);
current++;
cursor.continue();
} else {
// 已读取足够的条数,结束查询
resolve();
}
} else {
// 游标遍历结束(数据不足一页)
resolve();
}
};
});
// 总页数
result.totalPage = result.total > 0 ? Math.ceil(result.total / result.pageSize) : 0;
return result;
} catch (err) {
throw new Error(`分页查询失败:${err.message}`);
}
}
/**
* 按索引分页查询(适合按非主键字段排序分页)
* @param {string} storeName - 对象仓库名称
* @param {string} indexName - 索引名称
* @param {number} pageNum - 页码(从 1 开始)
* @param {number} pageSize - 每页条数
* @param {IDBKeyRange} [keyRange] - 可选:索引范围
* @param {boolean} [ascending=true] - 是否升序(false 为降序)
* @returns {Promise<{data: Array, total: number}>} 分页结果
*/
async paginationByIndex(storeName, indexName, pageNum, pageSize, keyRange, ascending = true) {
const store = this.getStore(storeName);
const result = { data: [], total: 0 };
try {
// 1. 获取索引
const index = store.index(indexName);
// 2. 统计符合条件的总条数
result.total = await new Promise((resolve) => {
const countRequest = keyRange
? index.count(keyRange)
: index.count();
countRequest.onsuccess = () => resolve(countRequest.result);
});
// 3. 计算分页起始位置
const skip = (pageNum - 1) * pageSize;
if (skip >= result.total) {
return result;
}
// 4. 按索引打开游标,支持排序和分页
await new Promise((resolve) => {
let current = 0;
// 打开索引游标(direction 控制排序:next 升序,prev 降序)
const request = index.openCursor(
keyRange,
ascending ? 'next' : 'prev'
);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (current < skip) {
// 跳过前 skip 条
current++;
cursor.continue();
} else if (current < skip + pageSize) {
// 读取当前页数据
result.data.push(cursor.value);
current++;
cursor.continue();
} else {
// 读取完毕
resolve();
}
} else {
// 数据不足一页
resolve();
}
};
});
return result;
} catch (err) {
throw new Error(`索引分页查询失败:${err.message}`);
}
}
}
export default IndexedDBUtil;
2、Vue3 使用
<template>
<div class="indexed-db-demo">
<h3>IndexedDB 用户管理示例</h3>
<!-- 新增/更新表单 -->
<div class="form">
<input
v-model="form.id"
placeholder="用户ID(主键,如 1001)"
type="number"
/>
<input
v-model="form.name"
placeholder="用户名"
/>
<input
v-model="form.phone"
placeholder="手机号(索引字段)"
/>
<button @click="handleSave">保存(新增/更新)</button>
<button @click="handleDelete" :disabled="!form.id">删除</button>
</div>
<!-- 查询区域 -->
<div class="query">
<input
v-model="queryKey"
placeholder="输入用户ID查询"
type="number"
/>
<button @click="handleQueryByKey">按ID查询</button>
<input
v-model="queryPhone"
placeholder="输入手机号查询"
/>
<button @click="handleQueryByPhone">按手机号查询</button>
</div>
<!-- 结果展示 -->
<div class="result">
<h4>查询结果:</h4>
<pre>{{ JSON.stringify(currentUser, null, 2) }}</pre>
</div>
<!-- 状态提示 -->
<div class="status" :class="{ success: statusType === 'success', error: statusType === 'error' }">
{{ statusMsg }}
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import IndexedDBUtil from '@/utils/indexedDBUtil.js';
// 1. 初始化 IndexedDB 实例
// 配置:数据库名=userDB,版本=1,对象仓库=users(主键=id,索引=phone(唯一))
const dbUtil = new IndexedDBUtil('userDB', 1, [
{
storeName: 'users', // 对象仓库名称(表名)
keyPath: 'id', // 主键字段(用户ID)
indexes: [
{ indexName: 'idx_phone', indexPath: 'phone', unique: true } // 索引:手机号唯一
]
}
]);
// 2. 组件状态
const form = ref({ id: '', name: '', phone: '' }); // 新增/更新表单
const queryKey = ref(''); // 按ID查询的关键词
const queryPhone = ref(''); // 按手机号查询的关键词
const currentUser = ref(null); // 当前查询结果
const statusMsg = ref(''); // 操作状态提示
const statusType = ref(''); // 状态类型:success/error
/**
* 3. 组件挂载时打开数据库
*/
onMounted(async () => {
try {
await dbUtil.openDB();
setStatus('success', '数据库连接成功');
} catch (err) {
setStatus('error', err.message);
}
});
/**
* 4. 组件卸载时关闭数据库
*/
onUnmounted(() => {
dbUtil.closeDB();
});
/**
* 5. 设置状态提示
*/
const setStatus = (type, msg) => {
statusType.value = type;
statusMsg.value = msg;
// 3秒后清除提示
setTimeout(() => {
statusMsg.value = '';
statusType.value = '';
}, 3000);
};
/**
* 6. 保存数据(新增/更新)
*/
const handleSave = async () => {
const { id, name, phone } = form.value;
if (!id || !name || !phone) {
setStatus('error', '请填写完整表单');
return;
}
try {
// 用 putData:主键存在则更新,不存在则新增
await dbUtil.putData('users', { id: Number(id), name, phone });
setStatus('success', '数据保存成功');
// 保存后清空表单
form.value = { id: '', name: '', phone: '' };
} catch (err) {
setStatus('error', err.message);
}
};
/**
* 7. 按主键(ID)查询
*/
const handleQueryByKey = async () => {
// let list = await dbUtil.fuzzySearch('users', "id", queryKey.value);
// console.log("数据", list)
const key = queryKey.value;
if (!key) {
setStatus('error', '请输入用户ID');
return;
}
try {
// const user = await dbUtil.getDataByKey('users', Number(key));
// const user = await dbUtil.fuzzySearch('users', "id", key);
const user = await dbUtil.paginationQuery('users', 1, 1);
currentUser.value = user;
setStatus('success', user ? '查询成功' : '未找到该用户');
} catch (err) {
setStatus('error', err.message);
}
};
/**
* 8. 按索引(手机号)查询
*/
const handleQueryByPhone = async () => {
const phone = queryPhone.value;
if (!phone) {
setStatus('error', '请输入手机号');
return;
}
try {
const user = await dbUtil.getDataByIndex('users', 'idx_phone', phone);
currentUser.value = user;
setStatus('success', user ? '查询成功' : '未找到该用户');
} catch (err) {
setStatus('error', err.message);
}
};
/**
* 9. 删除数据(按主键)
*/
const handleDelete = async () => {
const id = form.value.id;
if (!confirm(`确定要删除 ID 为 ${id} 的用户吗?`)) return;
try {
await dbUtil.deleteData('users', Number(id));
setStatus('success', '数据删除成功');
currentUser.value = null;
form.value = { id: '', name: '', phone: '' };
} catch (err) {
setStatus('error', err.message);
}
};
</script>
<style scoped>
.indexed-db-demo {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.form, .query {
display: flex;
gap: 10px;
margin: 15px 0;
align-items: center;
flex-wrap: wrap;
}
input {
padding: 8px;
flex: 1;
min-width: 150px;
}
button {
padding: 8px 16px;
cursor: pointer;
background: #409eff;
color: #fff;
border: none;
border-radius: 4px;
}
button:disabled {
background: #c0c4cc;
cursor: not-allowed;
}
.result {
margin: 15px 0;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.status {
margin: 10px 0;
padding: 8px;
border-radius: 4px;
}
.success {
background: #f0f9eb;
color: #67c23a;
}
.error {
background: #fef0f0;
color: #f56c6c;
}
</style>

更多推荐
所有评论(0)