当前位置: 首页 > 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.gsyq.cn/news/1507073.html

相关文章:

  • 从4K到2M:动手写个简易MMU模拟器,看页大小如何影响你的程序内存占用
  • VTK 9.2.0 + VS2019 + Qt5.8.0 保姆级编译配置指南(含内存泄漏检查开启)
  • 2026年纳滤设备行业深度分析:工艺选择、成本构成与供应商能力评估 - 优质品牌商家
  • SD-PPP:Photoshop AI插件终极免费指南,让设计创作如虎添翼
  • 30VIN,0.15A,0.8uA低功耗,稳压LDO,XZ6328
  • 【2026权威发布】重庆GEO优化服务商综合测评:五家机构横向对比与深度拆解 - 品牌官
  • Cursor Pro免费激活工具:解决AI编程助手试用限制的终极方案
  • IAR 9.10.1项目实战:用IELFTOOL搞定多段代码CRC校验与一键生成Bin/Hex文件
  • 风电机组Simulink教学模型:三叶片变桨+多策略偏航控制可调仿真环境
  • 如何永久备份微信聊天记录?WeChatMsg终极解决方案
  • 从寄信到直投:hixl单边通信库如何拆掉PD分离场景中的数据搬运墙——昇腾CANN计算基础层的跨步通信原语深度拆解
  • 告别VMware!手把手教你用Proxmox VE 8.0搭建国产UOS虚拟机(保姆级图文)
  • 专业级KMS智能激活工具:企业级Windows和Office批量激活的5大核心优势
  • OSGB转DOM/DSM实战:抗锯齿与精准去黑边技术解析
  • Agent 不是靠好 Prompt,而是靠循环跑到验收
  • 华为敏捷分布式WLAN项目交付实录:从AC上线、AP注册到业务调优的全流程复盘
  • 用Multisim和74LS190芯片,手把手教你搭建一个带整点报时的数字电子钟(附完整仿真文件)
  • QMT持仓查询进阶:除了股票代码和盈亏,这些隐藏数据字段你都知道怎么用吗?
  • Stata多元回归分析保姆级教程:从数据导入到F检验结果解读(附空气质量案例)
  • ROS2机器人导航:手把手教你用rviz插件保存和加载多点巡航路线(附JSON文件解析)
  • 告别数据孤岛:用慧集通控件在致远OA表单里一键调用ERP客户信息(附SQL配置详解)
  • VC6环境下纯C++实现的网页HTML源码获取工具(含工程+可执行文件)
  • sip(System Interface Protocol):CANN软件栈中最靠近硬件的NPU系统管理层全解析
  • 3步搞定B站字幕下载:告别繁琐操作,高效获取CC字幕
  • Claude 4.6 vs Gemini 2.0 Pro:推理之王和速度之王的终极对决
  • 避开Stata回归分析五大常见误区:你的F检验和R²真的用对了吗?
  • 免费PDF转高清图册全攻略:3种微信端工具实测+保姆级教程 - 时时资讯
  • Claude Code与Tongyi Wanxiang Wan MCP集成教程
  • 一文讲透|2026年最强AI论文平台榜单,高质初稿轻松写
  • 2026年观光列车制造厂家综合评估:技术实力与运营效益的双重考量 - 企业推荐官【官方】