Dexie.js 是一个优秀的 IndexedDB 封装库,结合 Vue 可以轻松实现客户端数据存储。
1. 安装与初始化
安装 Dexie.js
npm install dexie
# 或
yarn add dexie
创建数据库服务文件
在 src
目录下创建 db.js
文件:
import Dexie from 'dexie';
const db = new Dexie('VueDatabase');
// 定义数据库结构
db.version(1).stores({
todos: '++id, title, completed, createdAt',
notes: '++id, content, tags, updatedAt'
});
// 可选:添加示例数据
db.on('populate', () => {
db.todos.add({ title: '学习Dexie.js', completed: false, createdAt: new Date() });
db.notes.add({ content: '这是我的第一条笔记', tags: ['重要'], updatedAt: new Date() });
});
export default db;
2. 在 Vue 组件中使用
基本 CRUD 操作
<template>
<div>
<h2>待办事项</h2>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加新任务">
<ul>
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" v-model="todo.completed" @change="updateTodo(todo)">
<span :class="{ completed: todo.completed }">{{ todo.title }}</span>
<button @click="deleteTodo(todo.id)">删除</button>
</li>
</ul>
</div>
</template>
<script>
import db from '@/db';
export default {
data() {
return {
todos: [],
newTodo: ''
};
},
async created() {
await this.loadTodos();
},
methods: {
async loadTodos() {
this.todos = await db.todos.toArray();
},
async addTodo() {
if (!this.newTodo.trim()) return;
await db.todos.add({
title: this.newTodo,
completed: false,
createdAt: new Date()
});
this.newTodo = '';
await this.loadTodos();
},
async updateTodo(todo) {
await db.todos.update(todo.id, {
completed: todo.completed
});
},
async deleteTodo(id) {
await db.todos.delete(id);
await this.loadTodos();
}
}
};
</script>
<style>
.completed {
text-decoration: line-through;
color: #888;
}
</style>
3. 使用 Vue 插件形式
为了在整个应用中更方便地使用数据库,可以创建 Vue 插件:
创建插件 src/plugins/dexie.js
import Dexie from 'dexie';
const VueDexie = {
install(Vue) {
const db = new Dexie('VueAppDB');
db.version(1).stores({
settings: '++id, key, value',
userData: '++id, userId, data'
});
Vue.prototype.$db = db;
}
};
export default VueDexie;
在 main.js 中注册插件
import Vue from 'vue';
import App from './App.vue';
import DexiePlugin from './plugins/dexie';
Vue.use(DexiePlugin);
new Vue({
render: h => h(App)
}).$mount('#app');
在组件中使用
<script>
export default {
async mounted() {
// 使用插件提供的 $db
const settings = await this.$db.settings.toArray();
console.log(settings);
// 添加设置
await this.$db.settings.add({
key: 'theme',
value: 'dark'
});
}
};
</script>
4. 高级用法
响应式数据绑定
<template>
<div>
<h2>笔记列表</h2>
<div v-for="note in notes" :key="note.id">
<h3>{{ note.content }}</h3>
<p>标签: {{ note.tags.join(', ') }}</p>
</div>
</div>
</template>
<script>
import { liveQuery } from 'dexie';
import db from '@/db';
export default {
data() {
return {
notes: []
};
},
created() {
// 使用 liveQuery 实现响应式数据
this.subscription = liveQuery(
() => db.notes.orderBy('updatedAt').reverse().toArray()
).subscribe({
next: notes => {
this.notes = notes;
},
error: error => {
console.error('查询错误:', error);
}
});
},
beforeDestroy() {
// 组件销毁时取消订阅
this.subscription && this.subscription.unsubscribe();
}
};
</script>
复杂查询与分页
async function getPaginatedNotes(page = 1, pageSize = 10) {
const offset = (page - 1) * pageSize;
return await db.notes
.orderBy('updatedAt')
.reverse()
.offset(offset)
.limit(pageSize)
.toArray();
}
// 在组件中使用
export default {
methods: {
async loadNotes(page) {
this.notes = await getPaginatedNotes(page);
}
}
};
事务处理
async function transferData(fromId, toId, amount) {
await db.transaction('rw', db.userData, async () => {
const fromUser = await db.userData.get(fromId);
const toUser = await db.userData.get(toId);
if (!fromUser || !toUser) {
throw new Error('用户不存在');
}
if (fromUser.data.balance < amount) {
throw new Error('余额不足');
}
await db.userData.update(fromId, {
'data.balance': fromUser.data.balance - amount
});
await db.userData.update(toId, {
'data.balance': toUser.data.balance + amount
});
});
}
5. 最佳实践
错误处理:始终处理数据库操作的错误
try { await db.someTable.add(data); } catch (error) { console.error('操作失败:', error); this.$toast.error('保存失败'); }
性能优化:
批量操作使用
bulkAdd
,bulkPut
,bulkDelete
大量数据时使用分页
合理创建索引
数据同步:
// 与后端API同步的示例 async function syncTodos() { const localTodos = await db.todos.toArray(); const serverTodos = await api.getTodos(); // 比较并合并数据的逻辑... }
4.数据库升级处理:
db.version(2).stores({ todos: '++id, title, completed, createdAt, priority', notes: '++id, content, tags, updatedAt, isPinned' }).upgrade(tx => { return tx.table('todos').toCollection().modify(todo => { todo.priority = todo.priority || 'normal'; }); });
6. 完整示例:Todo 应用
<template> <div class="todo-app"> <h1>Vue Dexie Todo</h1> <div class="todo-form"> <input v-model="newTodo" @keyup.enter="addTodo" placeholder="输入任务并回车" > <select v-model="priority"> <option value="low">低优先级</option> <option value="normal">普通</option> <option value="high">高优先级</option> </select> <button @click="addTodo">添加</button> </div> <div class="filters"> <button @click="filter = 'all'">全部</button> <button @click="filter = 'active'">未完成</button> <button @click="filter = 'completed'">已完成</button> </div> <ul class="todo-list"> <li v-for="todo in filteredTodos" :key="todo.id" :class="{ completed: todo.completed, 'high-priority': todo.priority === 'high', 'low-priority': todo.priority === 'low' }"> <input type="checkbox" v-model="todo.completed" @change="updateTodo(todo)" > <span class="todo-title">{{ todo.title }}</span> <span class="todo-date">{{ formatDate(todo.createdAt) }}</span> <button @click="deleteTodo(todo.id)">删除</button> </li> </ul> <div class="stats"> <span>总计: {{ todos.length }} | 剩余: {{ activeCount }}</span> <button v-if="completedCount > 0" @click="clearCompleted"> 清除已完成 </button> </div> </div> </template> <script> import db from '@/db'; import { liveQuery } from 'dexie'; export default { data() { return { todos: [], newTodo: '', priority: 'normal', filter: 'all' }; }, created() { this.subscription = liveQuery( () => db.todos.orderBy('createdAt').toArray() ).subscribe({ next: todos => { this.todos = todos; }, error: error => { console.error('加载待办事项失败:', error); } }); }, computed: { filteredTodos() { switch (this.filter) { case 'active': return this.todos.filter(t => !t.completed); case 'completed': return this.todos.filter(t => t.completed); default: return this.todos; } }, activeCount() { return this.todos.filter(t => !t.completed).length; }, completedCount() { return this.todos.filter(t => t.completed).length; } }, methods: { formatDate(date) { return new Date(date).toLocaleDateString(); }, async addTodo() { if (!this.newTodo.trim()) return; try { await db.todos.add({ title: this.newTodo, completed: false, priority: this.priority, createdAt: new Date() }); this.newTodo = ''; } catch (error) { console.error('添加任务失败:', error); } }, async updateTodo(todo) { try { await db.todos.update(todo.id, { completed: todo.completed }); } catch (error) { console.error('更新任务失败:', error); } }, async deleteTodo(id) { try { await db.todos.delete(id); } catch (error) { console.error('删除任务失败:', error); } }, async clearCompleted() { try { const completedIds = this.todos .filter(t => t.completed) .map(t => t.id); await db.todos.bulkDelete(completedIds); } catch (error) { console.error('清除已完成任务失败:', error); } } }, beforeDestroy() { this.subscription && this.subscription.unsubscribe(); } }; </script> <style> .todo-app { max-width: 600px; margin: 0 auto; padding: 20px; } .todo-form { display: flex; margin-bottom: 20px; } .todo-form input { flex-grow: 1; padding: 8px; margin-right: 10px; } .filters { margin-bottom: 20px; } .todo-list { list-style: none; padding: 0; } .todo-list li { display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #eee; } .todo-title { flex-grow: 1; margin: 0 10px; } .todo-date { font-size: 0.8em; color: #888; margin-right: 10px; } .completed .todo-title { text-decoration: line-through; color: #888; } .high-priority { border-left: 3px solid red; padding-left: 7px; } .low-priority { opacity: 0.7; } .stats { margin-top: 20px; display: flex; justify-content: space-between; align-items: center; } </style>
7. 总结
在 Vue 中使用 Dexie.js 的关键点:
将数据库初始化代码封装为单独模块
使用
liveQuery
实现响应式数据绑定合理处理异步操作和错误
在组件销毁时取消订阅
对于复杂应用,考虑使用 Vuex/Pinia 管理状态
Dexie.js 与 Vue 的结合可以轻松实现离线优先的应用程序,提供良好的用户体验。