Vue3+TypeScript中IndexedDB使用指南
Vue 3 和 TypeScript 结合 IndexedDB 使用,能满足你大量结构化数据的本地存储需求(比如离线应用、缓存、用户数据)。这里为你提供一份使用指南,包含原生写法和封装库两种方式。
🗃️ Vue 3 与 TypeScript 中的 IndexedDB 使用指南
IndexedDB 是浏览器内置的一个事务型、面向对象的 NoSQL 数据库,允许你在客户端存储大量结构化数据(包括文件/BLOB)。其异步 API 的设计避免了阻塞 UI,相比 localStorage 和 SessionStorage,它提供了更大的存储空间、更丰富的数据操作方式以及更健壮的事务支持。
🔍 1. IndexedDB 基础概念
在 Vue 3 和 TypeScript 项目中使用 IndexedDB 前,需理解其核心概念。
1.1 与 localStorage 的简单对比
| 特性 | IndexedDB | localStorage |
|---|---|---|
| 存储容量 | 通常更大(数百MB甚至更多) | 通常较小(几MB) |
| 数据结构 | 对象存储(类似 NoSQL) | 仅键值对 |
| 查询能力 | 支持索引和复杂查询 | 仅按键查询 |
| 事务支持 | 是 | 否 |
| 阻塞性 | 异步操作,不阻塞 UI | 同步操作,可能阻塞 UI |
| 适用场景 | 大量结构化数据、离线应用 | 小量简单数据 |
1.2 核心概念
-
数据库 (Database):每个源(协议+域名+端口)可以创建多个数据库,每个数据库有唯一名称和版本号。
-
对象存储 (Object Store):数据库中的主要数据存储机制,类似于集合(MongoDB)或表(SQL)。每个对象存储可以定义一个键路径 (keyPath) 或使用键生成器 (key generator)。
-
索引 (Index):基于对象存储中对象的属性创建,允许高效查询特定字段。
-
事务 (Transaction):所有数据库操作都必须在事务中执行,确保数据的完整性(原子性、一致性、隔离性、持久性)。
-
游标 (Cursor):用于迭代对象存储或索引中的多条记录。
-
操作(CRUD):即创建 (Create)、读取 (Read)、更新 (Update)、删除 (Delete) 操作。
📦 2. 安装与设置
2.1 使用原生 IndexedDB API
IndexedDB 是浏览器内置 API,无需额外安装。但其基于事件的回调 API 可能使代码显得冗长。
2.2 使用封装库(推荐)
封装库能简化操作。以下是两个常见选择:
-
idb: 一个轻量级的、基于 Promise 的 IndexedDB 封装库。
bash
npm install idb
-
Dexie.js: 功能更丰富的 IndexedDB 封装,提供简洁的 API 和强大的查询能力。
bash
npm install dexie
🫷 3. 原生 IndexedDB API 的基本用法(配合 idb)
以下示例使用 idb 库和 TypeScript。
3.1 初始化数据库
首先创建一个数据库工具文件,例如 src/utils/indexedDb.ts:
typescript
// src/utils/indexedDb.ts
import { openDB, DBSchema, IDBPDatabase } from 'idb';
// 定义数据库结构(可选,但强烈推荐用于TypeScript类型检查)
interface MyDB extends DBSchema {
'users': { // 对象存储名
key: number; // 主键类型
value: { // 存储值的类型
id?: number; // 由于keyPath是id,此处需对应
name: string;
email: string;
createdAt: Date;
};
indexes: { 'by-email': string }; // 索引定义
};
'messages': {
key: number;
value: {
id?: number;
userId: number;
content: string;
timestamp: Date;
};
indexes: { 'by-user-id': number };
};
}
let dbPromise: Promise<IDBPDatabase<MyDB>> | null = null;
export const initDB = (): Promise<IDBPDatabase<MyDB>> => {
if (dbPromise) {
return dbPromise;
}
dbPromise = openDB<MyDB>('MyVueAppDB', 1, {
upgrade(db, oldVersion, newVersion, transaction) {
// 创建或升级对象存储
if (!db.objectStoreNames.contains('users')) {
const userStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
userStore.createIndex('by-email', 'email', { unique: true }); // 唯一索引
}
if (!db.objectStoreNames.contains('messages')) {
const messageStore = db.createObjectStore('messages', { keyPath:id, autoIncrement: true });
messageStore.createIndex('by-user-id', 'userId', { unique: false });
}
},
});
return dbPromise;
};
// 获取数据库实例的辅助函数
const getDB = async (): Promise<IDBPDatabase<MyDB>> => {
return await initDB();
};
3.2 定义数据模型(TypeScript 接口)
在 src/types/index.ts 或类似文件中定义 TypeScript 接口以确保类型安全:
typescript
// src/types/models.ts
export interface User {
id?: number; // 自增主键,创建时可选
name: string;
email: string;
createdAt: Date;
}
export interface Message {
id?: number;
userId: number;
content: string;
timestamp: Date;
}
3.3 通用 CRUD 操作封装
继续在 src/utils/indexedDb.ts 中添加通用操作函数:
typescript
// ... (接之前的 initDB 和 getDB)
// CRUD 操作
export const dbOperations = {
// 添加数据
async addItem<T extends keyof MyDB>(
storeName: T,
item: MyDB[T]['value']
): Promise<number> {
const db = await getDB();
return await db.add(storeName, item);
},
// 读取数据(根据主键)
async getItem<T extends keyof MyDB>(
storeName: T,
key: number
): Promise<MyDB[T]['value'] | undefined> {
const db = await getDB();
return await db.get(storeName, key);
},
// 通过索引读取数据
async getItemByIndex<T extends keyof MyDB>(
storeName: T,
indexName: keyof MyDB[T]['indexes'],
key: any
): Promise<MyDB[T]['value'] | undefined> {
const db = await getDB();
const tx = db.transaction(storeName, 'readonly');
const index = tx.store.index(indexName as string);
return await index.get(key);
},
// 更新数据
async updateItem<T extends keyof MyDB>(
storeName: T,
item: MyDB[T]['value']
): Promise<number> {
const db = await getDB();
return await db.put(storeName, item);
},
// 删除数据(根据主键)
async deleteItem<T extends keyof MyDB>(
storeName: T,
key: number
): Promise<void> {
const db = await getDB();
await db.delete(storeName, key);
},
// 获取所有数据
async getAllItems<T extends keyof MyDB>(storeName: T): Promise<MyDB[T]['value'][]> {
const db = await getDB();
return await db.getAll(storeName);
},
// 使用游标遍历(示例:获取特定用户的所有消息)
async getMessagesByUserId(userId: number): Promise<Message[]> {
const db = await getDB();
const tx = db.transaction('messages', 'readonly');
const index = tx.store.index('by-user-id');
let cursor = await index.openCursor(IDBKeyRange.only(userId));
const messages: Message[] = [];
while (cursor) {
messages.push(cursor.value);
cursor = await cursor.continue();
}
await tx.done;
return messages;
},
};
3.4 在 Vue 组件中使用
在 main.ts 中初始化数据库,或在首个需要的组件中初始化:
typescript
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { initDB } from './utils/indexedDb';
const app = createApp(App);
// 异步初始化数据库
initDB().then(() => {
app.mount('#app');
}).catch((error) => {
console.error('Failed to initialize database:', error);
});
在 Vue 组件(例如 src/components/UserManager.vue)中使用:
vue
<template>
<div class="user-manager">
<h2>用户管理</h2>
<form @submit.prevent="addUser">
<input v-model="newUser.name" placeholder="姓名" required />
<input v-model="newUser.email" type="email" placeholder="邮箱" required />
<button type="submit">添加用户</button>
</form>
<ul v-if="users.length">
<li v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.email }}
<button @click="deleteUser(user.id!)">删除</button>
</li>
</ul>
<p v-else>暂无用户数据</p>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { User } from '@/types/models';
import { dbOperations } from '@/utils/indexedDb';
const users = ref<User[]>([]);
const newUser = ref<Omit<User, 'id' | 'createdAt'>>({
name: '',
email: '',
});
const loadUsers = async () => {
try {
users.value = await dbOperations.getAllItems('users');
} catch (error) {
console.error('Failed to load users:', error);
}
};
const addUser = async () => {
try {
const userToAdd: Omit<User, 'id'> = {
...newUser.value,
createdAt: new Date(),
};
await dbOperations.addItem('users', userToAdd as User);
newUser.value = { name: '', email: '' }; // 重置表单
await loadUsers(); // 重新加载用户列表
} catch (error) {
console.error('Failed to add user:', error);
if ((error as Error).name === 'ConstraintError') {
alert('邮箱已存在!');
}
}
};
const deleteUser = async (userId: number) => {
try {
await dbOperations.deleteItem('users', userId);
await loadUsers(); // 重新加载用户列表
} catch (error) {
console.error('Failed to delete user:', error);
}
};
onMounted(() => {
loadUsers();
});
</script>
🧰 4. 使用 Dexie.js 简化操作
Dexie.js 提供了更简洁的 API。
4.1 安装与配置 Dexie
bash
npm install dexie
4.2 创建数据库类
创建 src/utils/MyDatabase.ts 文件:
typescript
// src/utils/MyDatabase.ts
import Dexie, { Table } from 'dexie';
import { User, Message } from '@/types/models';
export class MyAppDatabase extends Dexie {
users!: Table<User, number>; // number 是主键类型
messages!: Table<Message, number>;
constructor() {
super('MyVueAppDBWithDexie');
this.version(1).stores({
users: '++id, name, email, createdAt', // ++ 表示自增主键
messages: '++id, userId, content, timestamp, *userId' // * 表示多条目索引(非唯一)
});
// 可选:为索引添加更详细的配置
this.version(2).stores({
users: '++id, &email, name, createdAt', // & 表示唯一索引
messages: '++id, userId, content, timestamp'
}).upgrade(tx => {
// 版本升级逻辑,例如数据迁移
});
}
}
export const db = new MyAppDatabase();
4.3 使用 Dexie 进行 CRUD 操作
在 Vue 组件中使用 Dexie:
vue
<template>
<!-- 类似之前的模板 -->
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { User } from '@/types/models';
import { db } from '@/utils/MyDatabase';
const users = ref<User[]>([]);
const newUser = ref<Omit<User, 'id' | 'createdAt'>>({ name: '', email: '' });
const loadUsers = async () => {
try {
// Dexie 提供了丰富的查询API
users.value = await db.users
.where('createdAt').above(new Date(2020, 0, 1)) // 条件查询示例
.toArray();
// 或者简单获取所有用户
// users.value = await db.users.toArray();
} catch (error) {
console.error('Failed to load users:', error);
}
};
const addUser = async () => {
try {
await db.users.add({
...newUser.value,
createdAt: new Date(),
});
newUser.value = { name: '', email: '' };
await loadUsers();
} catch (error) {
console.error('Failed to add user:', error);
}
};
const deleteUser = async (userId: number) => {
try {
await db.users.delete(userId);
await loadUsers();
} catch (error) {
console.error('Failed to delete user:', error);
}
};
// 使用索引查询
const findUserByEmail = async (email: string) => {
try {
const user = await db.users.where('email').equals(email).first();
return user;
} catch (error) {
console.error('Failed to find user by email:', error);
return null;
}
};
onMounted(() => {
loadUsers();
});
</script>
💡 5. 高级用法与最佳实践
5.1 复杂查询与索引使用
有效的索引能显著提升查询性能。
typescript
// 使用原生API的范围查询(idb)
async getUsersCreatedAfter(date: Date): Promise<User[]> {
const db = await getDB();
const tx = db.transaction('users', 'readonly');
const index = tx.store.index('by-created-at'); // 假设你创建了这个索引
const range = IDBKeyRange.lowerBound(date);
return await index.getAll(range);
}
// 使用Dexie的范围查询
const recentUsers = await db.users
.where('createdAt')
.aboveOrEqual(new Date(2024, 0, 1))
.toArray();
// 复合条件查询(Dexie更简便)
const specificUsers = await db.users
.where('name').equals('John')
.and(user => user.email.endsWith('@example.com'))
.toArray();
5.2 事务处理
事务对于保证数据一致性至关重要。
typescript
// 使用原生API的事务(添加用户并同时添加一条消息)
async function addUserWithInitialMessage(userData: Omit<User, 'id'>, messageContent: string) {
const db = await getDB();
const tx = db.transaction(['users', 'messages'], 'readwrite');
try {
const userId = await tx.objectStore('users').add({
...userData,
createdAt: new Date(),
});
await tx.objectStore('messages').add({
userId,
content: messageContent,
timestamp: new Date(),
});
await tx.done; // 等待事务完成
return userId;
} catch (error) {
tx.abort(); // 中止事务
throw error;
}
}
// 使用Dexie的事务
await db.transaction('rw', db.users, db.messages, async () => {
const userId = await db.users.add({
name: 'John',
email: 'john@example.com',
createdAt: new Date(),
});
await db.messages.add({
userId,
content: 'Welcome message!',
timestamp: new Date(),
});
});
5.3 错误处理
稳健的错误处理能提升用户体验。
typescript
// 统一的错误处理函数
function handleDBError(error: unknown, operation: string) {
console.error(`Database error during ${operation}:`, error);
if (error instanceof DOMException) {
switch (error.name) {
case 'QuotaExceededError':
alert('存储空间不足,请清理后重试。');
break;
case 'ConstraintError':
alert('数据已存在或违反唯一约束。');
break;
default:
alert(`操作失败: ${error.message}`);
}
} else {
alert('发生未知错误,请重试或联系支持。');
}
}
// 在组件中使用
const addUser = async () => {
try {
// ... 添加用户的逻辑
} catch (error) {
handleDBError(error, '添加用户');
}
};
🛠️ 6. 调试与浏览器开发者工具
现代浏览器提供了良好的 IndexedDB 调试支持。
-
Chrome/Edge: 打开开发者工具(F12)→ Application 标签页 → IndexedDB。
-
Firefox: 开发者工具(F12)→ Storage 标签页 → IndexedDB。
-
Safari: 开发者工具(⌥⌘I)→ Storage 标签页 → IndexedDB。
在这里你可以:
-
👀 查看所有数据库和对象存储。
-
🔍 检查存储的数据。
-
🗑️ 手动删除或修改数据(谨慎操作)。
-
⚠️ 检查数据库结构和索引。
📊 原生 API 与 Dexie.js 对比
| 操作 | 原生 API (配合 idb) | Dexie.js |
|---|---|---|
| 初始化 | 需手动定义 schema 和升级逻辑 | 声明式 schema 定义,简化升级 |
| 查询 | 需使用游标和索引,代码稍显繁琐 | 链式查询 API,类似 LINQ,非常直观 |
| 事务 | 需显式创建和管理事务 | db.transaction 简化事务处理 |
| TypeScript 支持 | 良好,但需自行定义复杂类型 | 优秀,原生支持 |
| 学习曲线 | 较陡峭,需理解底层概念 | 较平缓,API 设计直观 |
| 灵活性 | 极高,可精细控制 | 较高,但受限于库提供的 API |
📝 7. 总结与实践建议
-
项目评估:对于简单应用或需要极致控制的情况,可考虑原生 API(配合
idb)。对于大多数需要复杂查询和良好开发体验的中大型项目,推荐使用 Dexie.js。 -
类型安全:务必使用 TypeScript 接口定义模型,充分利用类型检查。
-
版本管理:在应用发布初期仔细规划数据库 schema。后期 schema 变更需通过
version升级处理,并编写数据迁移逻辑。 -
错误处理:始终处理异步操作可能出现的错误,为用户提供友好提示。
-
性能考量:
-
为常用查询字段创建索引。
-
避免在单个事务中操作过多数据。
-
考虑大数据集的分页查询。
-
-
清理策略:对于缓存性质的数据,实现适当的清理机制(如基于时间或数量),避免占用过多磁盘空间。
更多推荐


所有评论(0)