Vue+IndexedDB+idb

如题,结合官方文档与网络帖子,利用AI整理

Vue中使用idb操作IndexedDB完整指南

基于官方文档与实际项目经验整理,采用循序渐进的方式帮助您快速掌握

前言

在现代前端开发中,我们经常需要在浏览器中存储大量数据。虽然localStorage是个不错的选择,但它有容量限制(通常5MB)且只能存储字符串。这时候,IndexedDB就成为了更好的选择。

为什么选择IndexedDB?

  • 大容量存储:通常是GB级别,远大于localStorage

  • 结构化数据:支持存储复杂的对象数据

  • 异步操作:不会阻塞主线程

  • 事务支持:确保数据操作的原子性

  • 索引功能:提高查询效率

idb库则是IndexedDB的一个优秀包装库,它将复杂的原生API转换为简洁的Promise接口,让我们可以用更现代的方式操作IndexedDB。

准备工作

安装idb

首先,我们需要安装idb库:

1
2
3
4
5
6
7
8
# 使用npm
npm install idb

# 使用yarn
yarn add idb

# 使用pnpm
pnpm add idb

基础概念理解

在开始编码之前,让我们先理解几个核心概念:

1. 数据库(Database)

  • 数据库是存储数据的容器

  • 每个数据库有唯一的名称

  • 数据库可以包含多个对象仓库

2. 对象仓库(Object Store)

  • 类似于关系数据库中的表

  • 每个对象仓库存储一组相关的数据

  • 每个对象仓库必须有一个主键

3. 事务(Transaction)

  • 一组操作的集合,要么全部成功,要么全部失败

  • 确保数据的一致性和完整性

4. 索引(Index)

  • 提高查询效率的重要工具

  • 可以基于对象的属性创建索引

快速开始:创建第一个数据库

让我们从最基础的开始,创建一个数据库和对象仓库。

Demo 1:创建数据库和对象仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/utils/database.js
import { openDB } from 'idb';

export async function initDatabase() {
// 打开数据库,如果不存在则创建
const db = await openDB('MyFirstDB', 1, {
// 数据库版本升级时触发
upgrade(db) {
console.log('数据库升级中...');

// 创建用户对象仓库
if (!db.objectStoreNames.contains('users')) {
db.createObjectStore('users', {
keyPath: 'id', // 主键字段
autoIncrement: true // 自动生成主键
});
}

// 创建商品对象仓库
if (!db.objectStoreNames.contains('products')) {
db.createObjectStore('products', {
keyPath: 'id'
});
}
}
});

console.log('数据库初始化完成');
return db;
}

代码解释:

  • openDB:打开或创建数据库的主要方法

  • 第一个参数:数据库名称

  • 第二个参数:数据库版本号

  • 第三个参数:配置对象,包含升级回调函数

如何验证?
打开浏览器的开发者工具(F12),在Application标签页中可以看到我们创建的数据库

数据操作基础

掌握了数据库的创建,接下来让我们学习最常用的CRUD操作。

Demo 2:添加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 添加用户数据
export async function addUser(userData) {
const db = await initDatabase();

try {
// 使用add方法添加数据
const userId = await db.add('users', {
...userData,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});

console.log('用户添加成功,ID:', userId);
return userId;
} catch (error) {
console.error('添加用户失败:', error);
throw error;
} finally {
db.close();
}
}

// 使用示例
addUser({
name: '张三',
email: 'zhangsan@example.com',
age: 25,
level: 1
});

关键点:

  • db.add(storeName, data):添加新数据

  • 如果主键已存在,会抛出错误

  • 返回新创建数据的主键

Demo 3:获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 根据ID获取用户
export async function getUserById(userId) {
const db = await initDatabase();

try {
const user = await db.get('users', userId);
console.log('获取到的用户:', user);
return user;
} catch (error) {
console.error('获取用户失败:', error);
throw error;
} finally {
db.close();
}
}

// 获取所有用户
export async function getAllUsers() {
const db = await initDatabase();

try {
const users = await db.getAll('users');
console.log('所有用户:', users);
return users;
} catch (error) {
console.error('获取所有用户失败:', error);
throw error;
} finally {
db.close();
}
}

Demo 4:更新数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export async function updateUser(userId, updateData) {
const db = await initDatabase();

try {
// 先获取当前用户数据
const user = await db.get('users', userId);

if (!user) {
throw new Error('用户不存在');
}

// 合并更新数据
const updatedUser = {
...user,
...updateData,
updatedAt: new Date().toISOString()
};

// 使用put方法更新数据
await db.put('users', updatedUser);
console.log('用户更新成功');
return updatedUser;
} catch (error) {
console.error('更新用户失败:', error);
throw error;
} finally {
db.close();
}
}

// 使用示例
updateUser(1, {
age: 26,
level: 2
});

重要区别:

  • add:只能添加新数据,如果主键存在会报错

  • put:可以添加或更新数据,如果主键存在则更新

Demo 5:删除数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export async function deleteUser(userId) {
const db = await initDatabase();

try {
await db.delete('users', userId);
console.log('用户删除成功');
return true;
} catch (error) {
console.error('删除用户失败:', error);
throw error;
} finally {
db.close();
}
}

// 清空整个对象仓库
export async function clearUsers() {
const db = await initDatabase();

try {
await db.clear('users');
console.log('用户表清空成功');
return true;
} catch (error) {
console.error('清空用户表失败:', error);
throw error;
} finally {
db.close();
}
}

事务处理详解

事务是数据库操作的重要概念,它确保一组操作要么全部成功,要么全部失败。

什么是事务?

想象一下银行转账的场景:

  1. 从账户A扣除1000元

  2. 向账户B添加1000元

这两个操作必须同时成功或同时失败,否则就会出现数据不一致的问题。这就是事务的作用。

Demo 6:使用事务进行批量操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
export async function batchAddUsers(usersData) {
const db = await initDatabase();

try {
// 开始一个读写事务
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');

const results = [];

// 批量添加用户
for (const userData of usersData) {
const userId = await store.add({
...userData,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
results.push(userId);
}

// 等待事务完成
await tx.done;

console.log('批量添加用户成功');
return results;
} catch (error) {
console.error('批量添加用户失败:', error);
throw error;
} finally {
db.close();
}
}

// 使用示例
batchAddUsers([
{ name: '李四', email: 'lisi@example.com', age: 30 },
{ name: '王五', email: 'wangwu@example.com', age: 28 }
]);

Demo 7:事务的错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
export async function transferPoints(fromUserId, toUserId, amount) {
const db = await initDatabase();

try {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');

// 获取两个用户的数据
const fromUser = await store.get(fromUserId);
const toUser = await store.get(toUserId);

// 验证数据
if (!fromUser) throw new Error('源用户不存在');
if (!toUser) throw new Error('目标用户不存在');
if (fromUser.points < amount) throw new Error('积分不足');

// 更新积分
await store.put({
...fromUser,
points: fromUser.points - amount,
updatedAt: new Date().toISOString()
});

await store.put({
...toUser,
points: toUser.points + amount,
updatedAt: new Date().toISOString()
});

await tx.done;
console.log('积分转账成功');
return true;
} catch (error) {
console.error('积分转账失败:', error);
throw error;
} finally {
db.close();
}
}

事务的ACID特性:

  • 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成

  • 一致性(Consistency):事务完成后,数据库状态必须保持一致

  • 隔离性(Isolation):并发事务之间相互隔离

  • 持久性(Durability):事务完成后,数据修改是永久性的

数据库版本管理

随着应用的发展,我们可能需要修改数据库结构。IndexedDB提供了版本管理机制来处理这个问题。

Demo 8:数据库版本升级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 升级到版本2
export async function upgradeDatabase() {
const db = await openDB('MyFirstDB', 2, {
upgrade(db, oldVersion, newVersion) {
console.log(`数据库升级: ${oldVersion} -> ${newVersion}`);

// 版本1到版本2的升级逻辑
if (oldVersion < 2) {
const userStore = db.transaction('users', 'readwrite').objectStore('users');

// 为用户表添加新字段的默认值
userStore.openCursor().then(function cursorIterate(cursor) {
if (!cursor) return;

const user = cursor.value;
if (user.points === undefined) {
user.points = 0; // 默认积分
user.level = 1; // 默认等级
cursor.update(user);
}

return cursor.continue().then(cursorIterate);
});

// 为用户表创建索引
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('name', 'name');

// 创建新的订单表
if (!db.objectStoreNames.contains('orders')) {
const orderStore = db.createObjectStore('orders', {
keyPath: 'id',
autoIncrement: true
});

// 为订单表创建索引
orderStore.createIndex('userId', 'userId');
orderStore.createIndex('status', 'status');
}
}
}
});

console.log('数据库升级完成');
return db;
}

版本管理最佳实践:

  1. 版本号只能递增:不能降级数据库版本

  2. 升级逻辑要完整:处理所有可能的版本升级路径

  3. 数据迁移要谨慎:确保数据在升级过程中不会丢失

  4. 测试充分:在生产环境部署前充分测试升级过程

索引的使用

索引是提高查询效率的重要工具。没有索引,查询可能需要遍历整个对象仓库。

Demo 9:创建和使用索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { openDB } from 'idb';

// 初始化数据库并创建索引
const initDB = async () => {
// 打开数据库:名称为'userDB',版本号为1(版本号必须是整数,升级时需递增)
return openDB('userDB', 1, {
// 数据库升级回调(版本号从低到高时触发,首次创建也会触发)
upgrade(db) {
// 1. 检查并创建'users'对象仓库(类似表)
if (!db.objectStoreNames.contains('users')) {
// 创建对象仓库,以'id'为主键(keyPath: 'id')
const userStore = db.createObjectStore('users', { keyPath: 'id' });

// 2. 为'users'仓库创建索引
// ① 为'email'字段创建索引(邮箱唯一,不允许重复)
userStore.createIndex(
'idx_email', // 索引名称(自定义,查询时需使用)
'email', // 索引关联的字段(即对象中的`email`属性)
{ unique: true } // 配置:true表示该字段值必须唯一(重复会报错)
);

// ② 为'age'字段创建索引(年龄可重复)
userStore.createIndex(
'idx_age', // 索引名称
'age', // 索引关联的字段(对象中的`age`属性)
{ unique: false } // 配置:false表示字段值可重复
);

// ③ 支持多字段索引(可选,如同时基于'name'和'age')
userStore.createIndex(
'idx_name_age',
['name', 'age'], // 多字段索引,基于对象的`name`和`age`属性
{ unique: false }
);
}
},
});
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 在数据库升级时创建索引(见Demo 8)

// 使用索引查询
export async function getUserByEmail(email) {
const db = await initDatabase();

try {
// 使用索引查询
const user = await db.getFromIndex('users', 'email', email);
console.log('通过邮箱查询到的用户:', user);
return user;
} catch (error) {
console.error('通过邮箱查询用户失败:', error);
throw error;
} finally {
db.close();
}
}

// 使用索引获取多个结果
export async function getUsersByName(name) {
const db = await initDatabase();

try {
// 创建查询范围
const range = IDBKeyRange.only(name);
const users = await db.getAllFromIndex('users', 'name', range);

console.log('通过姓名查询到的用户:', users);
return users;
} catch (error) {
console.error('通过姓名查询用户失败:', error);
throw error;
} finally {
db.close();
}
}

Demo 10:范围查询

IDBKeyRange 常用方法

  • IDBKeyRange.lowerBound(value, open): 大于等于 value(open 为 true 则不包含 value)

  • IDBKeyRange.upperBound(value, open): 小于等于 value(open 为 true 则不包含 value)

  • IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen): 范围在 [lower, upper] 之间

  • IDBKeyRange.only(value): 只匹配特定值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export async function getUsersByAgeRange(minAge, maxAge) {
const db = await initDatabase();

try {
// 创建年龄范围
const ageRange = IDBKeyRange.bound(minAge, maxAge);

// 假设我们已经为age字段创建了索引
const users = await db.getAllFromIndex('users', 'age', ageRange);

console.log(`年龄在${minAge}-${maxAge}之间的用户:`, users);
return users;
} catch (error) {
console.error('按年龄范围查询用户失败:', error);
throw error;
} finally {
db.close();
}
}

索引类型:

  • 唯一索引:确保索引字段的值唯一

  • 非唯一索引:允许索引字段的值重复

  • 复合索引:基于多个字段创建的索引

游标操作

对于大量数据的遍历和复杂查询,游标是非常有用的工具。

Demo 11:使用游标遍历数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
export async function iterateAllUsers(callback) {
const db = await initDatabase();

try {
//创建一个针对名为 'users' 的对象仓库的事务,事务模式为只读(readonly)
const tx = db.transaction('users', 'readonly');
//从当前事务中,拿到 'users' 这个对象仓库的操作句柄
const store = tx.objectStore('users');

await new Promise((resolve, reject) => {
const cursorRequest = store.openCursor();

cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;

if (cursor) {
// 调用回调函数处理当前记录
const shouldContinue = callback(cursor.value, cursor.key);

if (shouldContinue !== false) {
cursor.continue(); // 继续下一条记录
} else {
resolve(); // 停止遍历
}
} else {
resolve(); // 遍历完成
}
};

cursorRequest.onerror = (event) => {
reject(event.target.error);
};
});

await tx.done;
return true;
} catch (error) {
console.error('遍历用户数据失败:', error);
throw error;
} finally {
db.close();
}
}

// 使用示例
iterateAllUsers((user, key) => {
console.log(`用户ID: ${key}, 姓名: ${user.name}`);

// 如果用户年龄大于30,停止遍历
if (user.age > 30) {
return false;
}

return true;
});

Demo 12:游标分页查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
export async function getUsersByPage(page = 1, pageSize = 10) {
const db = await initDatabase();

try {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');

const users = [];
const totalCount = await store.count();
const totalPages = Math.ceil(totalCount / pageSize);
const skip = (page - 1) * pageSize;

let count = 0;

await new Promise((resolve, reject) => {
const cursorRequest = store.openCursor();

cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;

if (cursor) {
if (count >= skip && count < skip + pageSize) {
users.push(cursor.value);
}

count++;

if (count < skip + pageSize) {
cursor.continue();
} else {
resolve();
}
} else {
resolve();
}
};

cursorRequest.onerror = (event) => {
reject(event.target.error);
};
});

await tx.done;

return {
users,
pagination: {
page,
pageSize,
totalCount,
totalPages
}
};
} catch (error) {
console.error('分页查询用户失败:', error);
throw error;
} finally {
db.close();
}
}

在Vue组件中使用

现在让我们看看如何在Vue组件中集成这些数据库操作。

Demo 13:创建数据库管理的Composition API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// src/composables/useDatabase.js
import { ref, reactive, toRefs } from 'vue';
import {
initDatabase,
addUser,
getUserById,
getAllUsers,
updateUser,
deleteUser
} from '@/utils/database';

export function useDatabase(storeName) {
const state = reactive({
data: null,
loading: false,
error: null,
pagination: {
page: 1,
pageSize: 10,
totalCount: 0,
totalPages: 0
}
});

const resetState = () => {
state.data = null;
state.loading = false;
state.error = null;
};

const getById = async (id) => {
resetState();
state.loading = true;

try {
const data = await getUserById(id);
state.data = data;
return { success: true, data };
} catch (error) {
state.error = error.message;
return { success: false, error: error.message };
} finally {
state.loading = false;
}
};

const getAll = async () => {
resetState();
state.loading = true;

try {
const data = await getAllUsers();
state.data = data;
state.pagination.totalCount = data.length;
state.pagination.totalPages = 1;
return { success: true, data };
} catch (error) {
state.error = error.message;
return { success: false, error: error.message };
} finally {
state.loading = false;
}
};

const create = async (item) => {
state.loading = true;

try {
const id = await addUser(item);
await getAll(); // 重新获取列表
return { success: true, id };
} catch (error) {
state.error = error.message;
return { success: false, error: error.message };
} finally {
state.loading = false;
}
};

const update = async (id, item) => {
state.loading = true;

try {
const updatedItem = await updateUser(id, item);
await getAll(); // 重新获取列表
return { success: true, data: updatedItem };
} catch (error) {
state.error = error.message;
return { success: false, error: error.message };
} finally {
state.loading = false;
}
};

const remove = async (id) => {
state.loading = true;

try {
await deleteUser(id);
await getAll(); // 重新获取列表
return { success: true };
} catch (error) {
state.error = error.message;
return { success: false, error: error.message };
} finally {
state.loading = false;
}
};

return {
...toRefs(state),
getById,
getAll,
create,
update,
remove
};
}

Demo 14:用户管理组件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<template>
<div class="user-management">
<div class="header">
<h2>用户管理</h2>
<button @click="showCreateModal = true" class="btn btn-primary">
添加用户
</button>
</div>

<!-- 加载状态 -->
<div v-if="loading" class="loading">加载中...</div>

<!-- 错误提示 -->
<div v-else-if="error" class="error">{{ error }}</div>

<!-- 用户列表 -->
<div v-else class="user-list">
<table class="user-table">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
<th>年龄</th>
<th>积分</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in data" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.age }}</td>
<td>{{ user.points }}</td>
<td class="actions">
<button @click="handleEdit(user)" class="btn btn-edit">编辑</button>
<button @click="handleDelete(user.id)" class="btn btn-delete">删除</button>
</td>
</tr>
</tbody>
</table>
</div>

<!-- 创建用户模态框 -->
<div v-if="showCreateModal" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>添加用户</h3>
<button @click="showCreateModal = false" class="close-btn">&times;</button>
</div>
<div class="modal-body">
<form @submit.prevent="handleCreate">
<div class="form-group">
<label>姓名:</label>
<input v-model="newUser.name" required class="form-control" />
</div>
<div class="form-group">
<label>邮箱:</label>
<input v-model="newUser.email" type="email" required class="form-control" />
</div>
<div class="form-group">
<label>年龄:</label>
<input v-model.number="newUser.age" type="number" min="1" required class="form-control" />
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">创建</button>
<button type="button" @click="showCreateModal = false" class="btn btn-secondary">取消</button>
</div>
</form>
</div>
</div>
</div>
</div>
</template>

<script setup>
import { ref, reactive } from 'vue';
import { useDatabase } from '@/composables/useDatabase';

// 使用数据库Composition API
const {
data,
loading,
error,
getAll,
create,
update,
remove
} = useDatabase('users');

// 组件状态
const showCreateModal = ref(false);
const newUser = reactive({
name: '',
email: '',
age: 18,
points: 0
});

// 初始化加载用户列表
const init = async () => {
await getAll();
};

// 创建用户
const handleCreate = async () => {
const result = await create(newUser);
if (result.success) {
showCreateModal.value = false;
// 重置表单
newUser.name = '';
newUser.email = '';
newUser.age = 18;
newUser.points = 0;
}
};

// 编辑用户
const handleEdit = (user) => {
// 编辑逻辑...
};

// 删除用户
const handleDelete = async (userId) => {
if (confirm('确定要删除这个用户吗?')) {
await remove(userId);
}
};

// 页面加载时初始化
init();
</script>

<style scoped>
/* 组件样式... */

</style>

性能优化策略

1. 连接池管理

为了避免频繁创建和关闭数据库连接,我们可以使用连接池模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// src/utils/db-pool.js
import { openDB } from 'idb';

class DBConnectionPool {
constructor() {
this.pool = [];
this.maxConnections = 5;
this.waiting = [];
this.isClosed = false;
}

async getConnection() {
if (this.isClosed) {
throw new Error('连接池已关闭');
}

// 如果有空闲连接,直接返回
if (this.pool.length > 0) {
return this.pool.shift();
}

// 如果未达到最大连接数,创建新连接
if (this.pool.length + this.waiting.length < this.maxConnections) {
const db = await openDB('MyFirstDB', 2);
return db;
}

// 否则等待空闲连接
return new Promise((resolve) => {
this.waiting.push(resolve);
});
}

releaseConnection(db) {
if (this.isClosed) {
db.close();
return;
}

// 如果有等待的请求,直接分配连接
if (this.waiting.length > 0) {
const resolve = this.waiting.shift();
resolve(db);
return;
}

// 否则放回连接池
this.pool.push(db);
}

async close() {
this.isClosed = true;

// 关闭所有连接
for (const db of this.pool) {
await db.close();
}

// 拒绝所有等待的请求
for (const resolve of this.waiting) {
resolve(null);
}

this.pool = [];
this.waiting = [];
}
}

// 创建连接池实例
export const connectionPool = new DBConnectionPool();

// 使用连接池的数据库操作
export async function withConnection(operation) {
let db = null;

try {
db = await connectionPool.getConnection();

if (!db) {
throw new Error('无法获取数据库连接');
}

return await operation(db);
} finally {
if (db) {
connectionPool.releaseConnection(db);
}
}
}

2. 缓存策略

对于频繁访问的数据,我们可以添加内存缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// src/utils/cache-manager.js
class CacheManager {
constructor(options = {}) {
this.cache = new Map();
this.ttl = options.ttl || 5 * 60 * 1000; // 默认5分钟
this.maxSize = options.maxSize || 1000;
}

set(key, value, ttl = this.ttl) {
// 如果缓存已满,删除最旧的条目
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}

const expireTime = Date.now() + ttl;
this.cache.set(key, { value, expireTime });
}

get(key) {
const item = this.cache.get(key);

if (!item) {
return null;
}

// 检查是否过期
if (Date.now() > item.expireTime) {
this.cache.delete(key);
return null;
}

return item.value;
}

delete(key) {
this.cache.delete(key);
}

clear() {
this.cache.clear();
}
}

export const cache = new CacheManager({
ttl: 10 * 60 * 1000, // 10分钟
maxSize: 500
});

// 带缓存的数据获取
export async function getCachedUser(userId) {
const cacheKey = `user:${userId}`;

// 先检查缓存
const cachedUser = cache.get(cacheKey);
if (cachedUser) {
return cachedUser;
}

// 缓存未命中,从数据库获取
const user = await getUserById(userId);

if (user) {
// 更新缓存
cache.set(cacheKey, user);
}

return user;
}

3. 批量操作优化

对于大量数据的操作,使用批量处理可以显著提高性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export async function batchProcessUsers(operations) {
return withConnection(async (db) => {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');

const results = [];

for (const op of operations) {
let result;

switch (op.type) {
case 'add':
result = await store.add(op.data);
break;
case 'put':
result = await store.put(op.data);
break;
case 'delete':
result = await store.delete(op.key);
break;
default:
throw new Error(`不支持的操作类型: ${op.type}`);
}

results.push({
operation: op,
result,
success: true
});
}

await tx.done;
return results;
});
}

错误处理和最佳实践

1. 完善的错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// src/utils/error-handler.js
export class DatabaseError extends Error {
constructor(message, code, details = {}) {
super(message);
this.name = 'DatabaseError';
this.code = code;
this.details = details;
}
}

export function handleDatabaseError(error) {
console.error('数据库操作失败:', error);

// 根据错误类型返回用户友好的消息
if (error.name === 'ConstraintError') {
return new DatabaseError('数据约束违反', 'CONSTRAINT_ERROR', {
message: '数据已存在或格式不正确'
});
}

if (error.name === 'NotFoundError') {
return new DatabaseError('数据未找到', 'NOT_FOUND_ERROR', {
message: '请求的数据不存在'
});
}

if (error.name === 'TransactionInactiveError') {
return new DatabaseError('事务已关闭', 'TRANSACTION_INACTIVE_ERROR', {
message: '数据库事务已关闭,请重试操作'
});
}

// 默认错误
return new DatabaseError('数据库操作失败', 'UNKNOWN_ERROR', {
originalError: error.message
});
}

// 在组件中使用
export async function safeDatabaseOperation(operation) {
try {
return {
success: true,
data: await operation()
};
} catch (error) {
const handledError = handleDatabaseError(error);
return {
success: false,
error: handledError
};
}
}

2. 数据库备份和恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// src/utils/backup-manager.js
export async function backupDatabase() {
return withConnection(async (db) => {
const backup = {};

// 备份所有对象仓库的数据
for (const storeName of db.objectStoreNames) {
const tx = db.transaction(storeName, 'readonly');
const store = tx.objectStore(storeName);
backup[storeName] = await store.getAll();
await tx.done;
}

// 保存备份到localStorage或服务器
const backupData = {
timestamp: new Date().toISOString(),
version: db.version,
data: backup
};

localStorage.setItem('db_backup', JSON.stringify(backupData));
return backupData;
});
}

export async function restoreDatabase(backupData = null) {
const backup = backupData || JSON.parse(localStorage.getItem('db_backup'));

if (!backup) {
throw new Error('没有找到备份数据');
}

return withConnection(async (db) => {
// 恢复数据
for (const storeName in backup.data) {
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);

// 清空现有数据
await store.clear();

// 恢复备份数据
for (const item of backup.data[storeName]) {
await store.put(item);
}

await tx.done;
}

return true;
});
}

3. 浏览器兼容性处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// src/utils/compatibility.js
export function checkIndexedDBCompatibility() {
try {
// 检测IndexedDB支持
if (!window.indexedDB) {
return {
supported: false,
message: '您的浏览器不支持IndexedDB,请升级到现代浏览器'
};
}

// 检测Promise支持
if (!window.Promise) {
return {
supported: false,
message: '您的浏览器不支持Promise,请升级到现代浏览器'
};
}

return { supported: true };
} catch (error) {
return {
supported: false,
message: `兼容性检测失败: ${error.message}`
};
}
}

// 降级到localStorage的适配器
export class LocalStorageAdapter {
constructor(prefix = 'db_') {
this.prefix = prefix;
}

async get(storeName, key) {
const data = localStorage.getItem(this.prefix + storeName);
if (!data) return null;

const store = JSON.parse(data);
return store[key] || null;
}

async getAll(storeName) {
const data = localStorage.getItem(this.prefix + storeName);
if (!data) return [];

const store = JSON.parse(data);
return Object.values(store);
}

async add(storeName, value, key) {
const data = localStorage.getItem(this.prefix + storeName) || '{}';
const store = JSON.parse(data);

if (store[key]) {
throw new Error(`主键 ${key} 已存在`);
}

store[key] = value;
localStorage.setItem(this.prefix + storeName, JSON.stringify(store));
return key;
}

async put(storeName, value, key) {
const data = localStorage.getItem(this.prefix + storeName) || '{}';
const store = JSON.parse(data);

store[key] = value;
localStorage.setItem(this.prefix + storeName, JSON.stringify(store));
return key;
}

async delete(storeName, key) {
const data = localStorage.getItem(this.prefix + storeName);
if (!data) return;

const store = JSON.parse(data);
delete store[key];
localStorage.setItem(this.prefix + storeName, JSON.stringify(store));
}

async clear(storeName) {
localStorage.removeItem(this.prefix + storeName);
}
}

应用场景

IndexedDB特别适合以下场景:

  • 离线应用:PWA应用的离线数据存储

  • 大量数据:需要存储GB级别数据的应用

  • 复杂查询:需要高效查询和索引的应用

  • 数据同步:客户端数据缓存和服务器同步