'# 第二篇 electron + vue + sqlite3 桌面端集成本地数据库实现增删改查
一、背景与问题
在桌面端应用开发中,本地数据库是构建离线功能、数据持久化和复杂业务逻辑的核心组件。Electron 作为跨平台桌面应用框架,天然支持 Node.js 环境,而 SQLite3 作为轻量级关系型数据库,是本地存储的首选方案。然而,开发过程中常遇到以下问题:
- 进程隔离问题:Electron 的主进程(Main Process)与渲染进程(Renderer Process)的通信机制容易导致数据库连接异常
- 事务管理复杂性:多线程操作可能导致数据库锁竞争
- 数据一致性风险:前端直接操作数据库可能引发 SQL 注入漏洞
- 跨平台兼容性:不同操作系统下 SQLite3 的行为差异
本文将深入探讨如何在 Electron + Vue 项目中安全、高效地集成 SQLite3,构建可扩展的本地数据库系统。
二、基本原理
1. Electron 进程架构
Electron 采用主进程(Main Process)和渲染进程(Renderer Process)分离架构:
- 主进程:负责创建窗口、管理系统资源、处理底层逻辑
- 渲染进程:负责 UI 渲染,通过 IPC(Inter-Process Communication)与主进程通信
SQLite3 的数据库操作必须在主进程中执行,因渲染进程无法直接访问 Node.js 模块(如 sqlite3)。通过 IPC 实现进程间通信是关键。
2. SQLite3 的工作原理
SQLite3 是一个嵌入式数据库,其核心特点包括:
- 文件存储:所有数据存储在单个文件中
- 无服务器架构:无需独立数据库服务器
- ACID 事务支持:支持原子性、一致性、隔离性和持久性
在 Electron 中,SQLite3 的使用需注意以下约束:
- 主进程初始化数据库连接
- 使用
sqlite3.Database创建连接 - 通过
ipcMain监听前端请求 - 避免在渲染进程直接使用 sqlite3 模块
三、环境准备
1. 项目依赖
npm install electron vue sqlite32. 项目结构建议
my-electron-app/
├── main.js # 主进程入口
├── index.html # 渲染进程入口
├── App.vue # Vue 组件
├── database.js # SQLite3 工具类
├── package.json
└── README.md3. 环境配置
在 main.js 中配置窗口和 IPC 通信:
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: true
}
});
mainWindow.loadFile('index.html');
// IPC 通信监听
ipcMain.on('db-query', (event, query, params) => {
// 调用数据库操作方法
});
}
app.whenReady().then(createWindow);四、核心实现
1. 数据库连接初始化
// database.js
const { Database } = require('sqlite3');
const path = require('path');
class SQLiteDatabase {
constructor(dbPath) {
this.dbPath = path.resolve(dbPath);
this.db = new Database(this.dbPath, (err) => {
if (err) {
console.error('Database connection error:', err.message);
}
});
}
async query(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.serialize(() => {
this.db.all(sql, params, (err, rows) => {
if (err) {
reject(err);
return;
}
resolve(rows);
});
});
});
}
async run(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.serialize(() => {
this.db.run(sql, params, function(err) {
if (err) {
reject(err);
return;
}
resolve(this.lastID); // 返回自增ID
});
});
});
}
close() {
return new Promise((resolve, reject) => {
this.db.close((err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
}
module.exports = SQLiteDatabase;2. 增删改查实现
// database.js (扩展部分)
async function createTable() {
await this.query(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT 0
)
`);
}
async function insertTask(title) {
const id = await this.run(
'INSERT INTO tasks (title) VALUES (?)',
[title]
);
return id;
}
async function getTasks() {
return await this.query('SELECT * FROM tasks');
}
async function updateTask(id, completed) {
await this.run(
'UPDATE tasks SET completed = ? WHERE id = ?',
[completed, id]
);
}
async function deleteTask(id) {
await this.run(
'DELETE FROM tasks WHERE id = ?',
[id]
);
}3. 跨进程通信
// main.js (扩展部分)
const db = new SQLiteDatabase('./db/tasks.db');
db.createTable();
ipcMain.on('db-insert', (event, title) => {
db.insertTask(title)
.then(id => {
event.reply('db-insert-response', id);
})
.catch(err => {
event.reply('db-insert-error', err.message);
});
});
ipcMain.on('db-get', (event) => {
db.getTasks()
.then(tasks => {
event.reply('db-get-response', tasks);
})
.catch(err => {
event.reply('db-get-error', err.message);
});
});五、完整案例
1. 基于 Vue 的待办事项应用
(1) 前端组件 (App.vue)
<template>
<div>
<input v-model="newTask" @keyup.enter="addTask" placeholder="输入任务" />
<button @click="addTask">添加</button>
<ul>
<li v-for="task in tasks" :key="task.id">
{{ task.title }} - {{ task.completed ? '完成' : '未完成' }}
<button @click="toggleTask(task.id)">切换状态</button>
<button @click="deleteTask(task.id)">删除</button>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
newTask: '',
tasks: []
};
},
methods: {
addTask() {
if (!this.newTask.trim()) return;
this.$electron.ipcRenderer.send('db-insert', this.newTask);
this.newTask = '';
},
toggleTask(id) {
this.$electron.ipcRenderer.send('db-toggle', id);
},
deleteTask(id) {
this.$electron.ipcRenderer.send('db-delete', id);
},
getTasks() {
this.$electron.ipcRenderer.send('db-get');
}
},
mounted() {
this.getTasks();
}
};
</script>(2) 后端扩展 (main.js)
// main.js (扩展部分)
ipcMain.on('db-toggle', (event, id) => {
db.updateTask(id, 1 - (db.tasks.find(t => t.id === id)?.completed || 0))
.then(() => {
event.reply('db-toggle-response');
})
.catch(err => {
event.reply('db-toggle-error', err.message);
});
});
ipcMain.on('db-delete', (event, id) => {
db.deleteTask(id)
.then(() => {
event.reply('db-delete-response');
})
.catch(err => {
event.reply('db-delete-error', err.message);
});
});六、源码解析
1. 数据库连接管理
在 database.js 中的 SQLiteDatabase 类实现了连接管理:
- 使用
path.resolve确保路径正确性 - 通过
serialize方法确保数据库操作的原子性 - 使用 Promise 包装异步操作,便于前端调用
2. SQL 注入防护
在 query 方法中使用参数化查询:
this.db.all(sql, params, (err, rows) => {
// 处理结果
});通过将参数与 SQL 语句分离,有效防止 SQL 注入攻击。
3. 错误处理机制
在 IPC 通信中,通过 event.reply 返回错误信息:
event.reply('db-insert-error', err.message);前端通过 window.electron.ipcRenderer.on 监听错误:
window.electron.ipcRenderer.on('db-insert-error', (event, message) => {
alert('插入任务失败: ' + message);
});七、进阶使用
1. 事务处理
对于需要原子性操作的场景(如批量更新),可以使用事务:
async function batchUpdate(tasks) {
return new Promise((resolve, reject) => {
this.db.serialize(() => {
this.db.beginTransaction(() => {
tasks.forEach(task => {
this.db.run(
'UPDATE tasks SET completed = ? WHERE id = ?',
[task.completed, task.id]
);
});
this.db.commit(() => {
resolve();
}, (err) => {
this.db.rollback();
reject(err);
});
});
});
});
}2. 索引优化
在频繁查询字段上创建索引:
await this.query(`
CREATE INDEX IF NOT EXISTS idx_title
ON tasks (title)
`);3. 分页查询
处理大数据量时使用分页:
async function getTasks(page = 1, limit = 20) {
return await this.query(
'SELECT * FROM tasks ORDER BY id DESC LIMIT ? OFFSET ?',
[limit, (page - 1) * limit]
);
}八、性能与工程实践
1. 性能优化策略
| 优化策略 | 说明 |
|---|---|
| 索引优化 | 在查询字段上创建索引,提升查询速度 |
| 批量操作 | 使用事务处理多条记录更新 |
| 缓存机制 | 对高频查询结果进行缓存 |
| 分页处理 | 避免一次性加载大量数据 |
| 事务控制 | 适当使用事务,避免不必要的锁竞争 |
2. 异常处理规范
- 所有数据库操作必须包含错误处理
- 对于关键操作(如删除),应添加确认机制
- 使用
try/catch包裹数据库操作 - 对于持久化存储,设置超时机制
3. 安全实践
- 禁用
nodeIntegration时使用contextIsolation - 对用户输入进行严格校验
- 使用
sqlite3的stringify功能处理特殊字符 - 定期清理数据库文件
九、常见问题与踩坑
1. 常见错误及解决办法
| 错误类型 | 原因 | 解决办法 |
|---|---|---|
Cannot access 'sqlite3' from renderer | 渲染进程未正确配置 | 在 webPreferences 中设置 nodeIntegration: true |
SQLite3 database file not found | 路径配置错误 | 使用 path.resolve 确保路径正确 |
Database is locked | 多线程操作冲突 | 使用 serialize 确保操作顺序 |
SQL injection vulnerability | 用户输入未过滤 | 使用参数化查询 |
2. 典型问题分析
问题:数据库文件丢失
原因:未正确配置数据库路径,或文件权限不足
解决方法:
const dbPath = path.resolve(__dirname, 'db/tasks.db');问题:性能瓶颈
原因:频繁的全表查询
解决方法:添加索引并使用分页查询
十、最佳实践
1. 推荐使用场景
- 需要离线功能的桌面应用
- 数据量不大(建议单表不超过10万条)
- 不需要复杂的查询逻辑
- 跨平台支持要求高
2. 不推荐使用场景
- 需要高并发访问(SQLite 为单线程)
- 需要复杂事务处理
- 需要网络同步功能
- 需要大规模数据处理(建议使用 MongoDB 等 NoSQL)
3. 推荐的实现模式
- 使用工具类封装数据库操作
- 所有数据库操作都通过 IPC 通信
- 对关键操作添加确认机制
- 定期清理数据库文件
- 使用索引优化查询性能
十一、总结
本文深入探讨了 Electron + Vue + SQLite3 的集成实现,重点分析了进程通信机制、数据库连接管理、SQL 注入防护和性能优化等关键问题。通过完整案例展示了如何构建可扩展的本地数据库系统,同时指出了实际开发中需要特别注意的陷阱和最佳实践。
在实际项目中,这种方案适用于需要离线功能的桌面应用,但需注意其局限性。对于需要高并发或复杂查询的场景,建议采用更专业的数据库系统。通过合理的设计和规范的实现,可以构建出稳定、高效的本地数据库解决方案。