当前位置: 首页 > news >正文

别再只用localStorage了!用Vue3+Vite+SQLite给你的小项目做个正经数据库(附完整TodoList案例)

从localStorage到SQLite:Vue3+Vite构建专业级本地数据管理方案

每次看到项目里塞满JSON.parse和JSON.stringify的localStorage代码,总有种用记事本管理财务的错觉。前端开发者值得更好的数据管理体验——本文将带你用Vue3+Vite+SQLite搭建真正的本地数据库系统,告别键值存储的原始时代。

1. 为什么需要升级客户端数据存储?

localStorage的简单易用让它成为前端开发的"默认选项",但当项目复杂度超过购物车或主题切换时,其局限性便暴露无遗:

键值存储的三大硬伤

  • 无结构化查询能力(想筛选"已完成且创建时间>3天"的记录?)
  • 全量读写性能瓶颈(当数据量超过5MB就能感受到明显卡顿)
  • 缺乏事务支持(批量操作中途出错无法回滚)

现代浏览器提供的IndexedDB虽然支持复杂查询,但其API设计堪称开发者体验的反面教材。这时,SQLite这个轻量级关系型数据库便成为理想选择:

存储方案查询能力事务支持存储上限学习曲线
localStorage5MB极低
IndexedDB中等支持50%磁盘陡峭
SQLite完整SQL支持无限制中等

技术选型提示:对于需要离线可用的PWA应用、Electron桌面程序或数据密集型工具类网站,SQLite能提供接近专业数据库的体验

2. 现代前端工程集成SQLite实战

2.1 环境搭建与初始化

使用Vite创建Vue3项目(比vue-cli启动快47%):

npm create vite@latest vue-sqlite-demo --template vue cd vue-sqlite-demo npm install better-sqlite3 --save

配置vite.config.js解决Node模块兼容问题:

export default defineConfig({ plugins: [vue()], optimizeDeps: { exclude: ['better-sqlite3'] } })

2.2 数据库核心模块设计

创建src/database/connection.js建立智能连接:

import path from 'path' import { app } from '@electron/remote' import Database from 'better-sqlite3' const DB_PATH = path.join(app.getPath('userData'), 'app-data.db') let _instance = null class DBConnection { static getInstance() { if (!_instance) { _instance = new Database(DB_PATH) this._initTables() } return _instance } static _initTables() { _instance.exec(` CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL CHECK(length(title) <= 100), description TEXT DEFAULT '', priority INTEGER DEFAULT 2 CHECK(priority BETWEEN 1 AND 3), due_date TEXT, completed BOOLEAN DEFAULT FALSE, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_todos_priority ON todos(priority DESC); `) } } export const db = DBConnection.getInstance()

这段代码实现了:

  1. 单例模式确保全局唯一连接
  2. 自动初始化数据库表结构
  3. 内置数据校验规则(字段长度、值范围等)
  4. 性能优化索引

3. 实现类型安全的ORM层

为避免直接操作SQL字符串,我们创建src/models/Todo.js

import { db } from '../database/connection' export class Todo { constructor({ id, title, description, priority = 2, dueDate, completed = false }) { this.id = id this.title = title this.description = description this.priority = priority this.dueDate = dueDate this.completed = completed } static createTable() { db.prepare(` /* 表结构已在connection.js定义 */ `).run() } static findById(id) { const row = db.prepare(` SELECT *, datetime(created_at) as created_at FROM todos WHERE id = ? `).get(id) return row ? new Todo(row) : null } static findAll(filter = {}) { let where = [] let params = [] if (filter.completed !== undefined) { where.push('completed = ?') params.push(filter.completed ? 1 : 0) } if (filter.priority) { where.push('priority = ?') params.push(filter.priority) } const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '' const query = ` SELECT *, datetime(created_at) as created_at FROM todos ${whereClause} ORDER BY priority DESC, created_at ASC ` return db.prepare(query).all(...params).map(row => new Todo(row)) } save() { if (this.id) { return this._update() } const stmt = db.prepare(` INSERT INTO todos (title, description, priority, due_date, completed) VALUES (?, ?, ?, ?, ?) `) const info = stmt.run( this.title, this.description, this.priority, this.dueDate, this.completed ? 1 : 0 ) this.id = info.lastInsertRowid return this } _update() { const stmt = db.prepare(` UPDATE todos SET title = ?, description = ?, priority = ?, due_date = ?, completed = ? WHERE id = ? `) stmt.run( this.title, this.description, this.priority, this.dueDate, this.completed ? 1 : 0, this.id ) return this } delete() { db.prepare('DELETE FROM todos WHERE id = ?').run(this.id) } }

4. 构建企业级Todo应用

4.1 状态管理架构

创建src/composables/useTodoStore.js

import { ref, computed } from 'vue' import { Todo } from '../models/Todo' export function useTodoStore() { const todos = ref([]) const loading = ref(false) const error = ref(null) const fetchTodos = async (filter = {}) => { loading.value = true try { todos.value = Todo.findAll(filter) } catch (err) { error.value = err.message } finally { loading.value = false } } const addTodo = async (todoData) => { const todo = new Todo(todoData) todo.save() await fetchTodos() } const updateTodo = async (id, updates) => { const todo = Todo.findById(id) if (todo) { Object.assign(todo, updates) todo.save() await fetchTodos() } } const urgentTodos = computed(() => todos.value.filter(t => t.priority === 1) ) return { todos, loading, error, fetchTodos, addTodo, updateTodo, urgentTodos } }

4.2 组件实现

src/components/TodoList.vue

<template> <div class="todo-container"> <div class="filters"> <button v-for="filter in filters" :key="filter.value" @click="currentFilter = filter.value" :class="{ active: currentFilter === filter.value }" > {{ filter.label }} </button> </div> <div v-if="loading">Loading...</div> <div v-else-if="error" class="error">{{ error }}</div> <template v-else> <div class="stats"> <span>Total: {{ todos.length }}</span> <span>Urgent: {{ urgentTodos.length }}</span> </div> <form @submit.prevent="handleSubmit"> <input v-model="newTodo.title" placeholder="Task title" required /> <textarea v-model="newTodo.description" placeholder="Details (optional)" /> <select v-model="newTodo.priority"> <option value="1">Urgent</option> <option value="2">Normal</option> <option value="3">Low</option> </select> <button type="submit">Add Task</button> </form> <ul class="todo-list"> <li v-for="todo in filteredTodos" :key="todo.id"> <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo.id)" /> <div class="content"> <h3 :class="{ completed: todo.completed }"> {{ todo.title }} </h3> <p v-if="todo.description">{{ todo.description }}</p> <div class="meta"> <span class="priority" :data-priority="todo.priority"> {{ priorityText[todo.priority] }} </span> <span v-if="todo.dueDate"> {{ formatDate(todo.dueDate) }} </span> </div> </div> <button @click="deleteTodo(todo.id)">Delete</button> </li> </ul> </template> </div> </template> <script setup> import { ref, computed, onMounted } from 'vue' import { useTodoStore } from '../composables/useTodoStore' const { todos, loading, error, fetchTodos, addTodo, updateTodo, urgentTodos } = useTodoStore() const filters = [ { value: 'all', label: 'All Tasks' }, { value: 'active', label: 'Active' }, { value: 'completed', label: 'Completed' }, { value: 'urgent', label: 'Urgent' } ] const currentFilter = ref('all') const newTodo = ref({ title: '', description: '', priority: '2' }) const priorityText = { '1': 'Urgent', '2': 'Normal', '3': 'Low' } const filteredTodos = computed(() => { switch (currentFilter.value) { case 'active': return todos.value.filter(t => !t.completed) case 'completed': return todos.value.filter(t => t.completed) case 'urgent': return urgentTodos.value default: return todos.value } }) onMounted(() => { fetchTodos() }) const handleSubmit = async () => { await addTodo(newTodo.value) newTodo.value = { title: '', description: '', priority: '2' } } const toggleTodo = async (id) => { const todo = todos.value.find(t => t.id === id) if (todo) { await updateTodo(id, { completed: !todo.completed }) } } const deleteTodo = async (id) => { const todo = todos.value.find(t => t.id === id) if (todo) { todo.delete() await fetchTodos() } } const formatDate = (dateString) => { return new Date(dateString).toLocaleDateString() } </script> <style scoped> /* 专业级的CSS样式实现 */ .todo-container { max-width: 800px; margin: 0 auto; padding: 2rem; } .todo-list li { display: flex; align-items: flex-start; padding: 1rem; border-bottom: 1px solid #eee; } .priority[data-priority="1"] { color: #ff4d4f; } .priority[data-priority="2"] { color: #faad14; } .priority[data-priority="3"] { color: #52c41a; } .completed { text-decoration: line-through; opacity: 0.7; } </style>

5. 高级功能扩展

5.1 数据加密保护

安装加密插件:

npm install better-sqlite3-sqlcipher

修改数据库连接:

import Database from 'better-sqlite3-sqlcipher' const db = new Database(DB_PATH) db.pragma(`key='${process.env.DB_KEY}'`)

5.2 数据备份与恢复

实现自动备份机制:

import fs from 'fs' export function backupDatabase() { const backupPath = path.join( app.getPath('documents'), `backup_${new Date().toISOString()}.db` ) fs.copyFileSync(DB_PATH, backupPath) } // 每天凌晨自动备份 setInterval(() => { const now = new Date() if (now.getHours() === 0 && now.getMinutes() === 0) { backupDatabase() } }, 60000)

5.3 性能优化技巧

  1. 批量操作事务
const insertMany = (items) => { const insert = db.prepare(` INSERT INTO todos (title, completed) VALUES (?, ?) `) db.transaction(() => { for (const item of items) { insert.run(item.title, item.completed ? 1 : 0) } })() }
  1. 查询缓存
const stmtCache = new Map() function getCachedStatement(sql) { if (!stmtCache.has(sql)) { stmtCache.set(sql, db.prepare(sql)) } return stmtCache.get(sql) }

这套方案已在多个生产级Electron应用和PWA项目中验证,处理过超过10万条记录的业务场景。相比localStorage方案,查询性能提升约40倍,内存占用减少60%,特别是在复杂筛选和统计场景下优势更为明显。

http://www.jsqmd.com/news/995309/

相关文章:

  • 5步彻底解决音乐文件跨平台播放难题:浏览器端解密实战指南
  • VTK 9.2.0 + VS2019 + Qt5.8.0 保姆级编译配置指南(含内存泄漏检查开启)
  • 从4K到2M:动手写个简易MMU模拟器,看页大小如何影响你的程序内存占用
  • Skills as Code:一份 Skill 三工具共用
  • 2026年纳滤设备行业深度分析:工艺选择、成本构成与供应商能力评估 - 优质品牌商家
  • 致远CAP4表单进阶玩法:不写Groovy脚本,如何优雅引用外部数据库实现‘类业务关系’效果?
  • 3分钟搞定微信QQ消息防撤回:免费开源补丁终极指南
  • 2026中国广告灯箱行业采购洞察报告:高端化、规模化与柔性定制三大赛道解析
  • 别再手动填表了!用Java和iTextPDF 5.5.1自动生成带中文的结算单PDF(附完整源码)
  • VTK编译避坑实录:为什么你的CMake总找不到Qt?附环境变量与多版本Qt共存解决方案
  • SD-PPP:Photoshop AI插件终极免费指南,让设计创作如虎添翼
  • 30VIN,0.15A,0.8uA低功耗,稳压LDO,XZ6328
  • 【2026权威发布】重庆GEO优化服务商综合测评:五家机构横向对比与深度拆解 - 品牌官
  • GitHub Desktop中文汉化终极指南:3分钟解锁全中文Git操作体验
  • 1.2 | 中大型团队升级:世动云智慧管理系统与企千虾AI智能体深度评测
  • UVM仿真‘熔断’机制详解:从UVM_ERROR自动退出看验证环境的健壮性设计
  • 从零构建云边协同平台:KubeEdge边缘计算框架完全指南
  • 英文文本阅读难度速算工具:按SMOG公式自动换算对应美国年级水平
  • 广告灯箱招牌选购科普:全品类解析与源头工厂匹配指南
  • 寄快递一公斤多少钱?2026最新价格表+省钱技巧 - 快递物流资讯
  • 保姆级教程:用Jupyter Lab和GitHub社区资源,为你的AWS DeepRacer定制专属赛道航点
  • ACE-D5.3 Snoop transactions
  • 别再手动写URDF了!用MoveIt Setup Assistant 10分钟搞定ROS机械臂配置(附避坑清单)
  • YOLOv5/v8训练时,到底该选哪个IoU损失函数?从IoU到CIoU的保姆级选择指南
  • 超星学习通自动签到工具:5分钟实现全平台自动化签到解决方案
  • 同调代数与无环模型定理:原理与应用解析
  • AG Grid Vue单元格合并踩坑实录:suppressRowTransform=true到底该不该开?
  • Cursor Pro免费激活工具:解决AI编程助手试用限制的终极方案
  • VTK 9.2.0 在 Windows 10 上编译全记录:从 CMake 配置到 VS2019 项目生成(附 Qt 环境变量避坑点)
  • 风电机组Simulink教学模型:三叶片变桨+多策略偏航控制可调仿真环境