Vue 中使用 Dexie.js

发布于:2025-08-01 ⋅ 阅读:(22) ⋅ 点赞:(0)

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. 最佳实践

  1. 错误处理:始终处理数据库操作的错误
     

    try {
      await db.someTable.add(data);
    } catch (error) {
      console.error('操作失败:', error);
      this.$toast.error('保存失败');
    }
  2. 性能优化

    • 批量操作使用 bulkAddbulkPutbulkDelete

    • 大量数据时使用分页

    • 合理创建索引

  3. 数据同步
     

    // 与后端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 的关键点:

  1. 将数据库初始化代码封装为单独模块

  2. 使用 liveQuery 实现响应式数据绑定

  3. 合理处理异步操作和错误

  4. 在组件销毁时取消订阅

  5. 对于复杂应用,考虑使用 Vuex/Pinia 管理状态

Dexie.js 与 Vue 的结合可以轻松实现离线优先的应用程序,提供良好的用户体验。