桌面应用与 Web 应用最大的区别之一,就是用户希望数据能够“留”在本地——离线可用、启动即显、隐私可控。Electron 为你提供了三类截然不同的本地存储方案,每一类都对应着特定的场景和取舍。这一节不会罗列所有可能的选项,而是聚焦在实际项目中真正会用到的那三个:IndexedDB、本地文件存储和 SQLite。
12.5.1 IndexedDB:浏览器原生,零依赖
IndexedDB 是 Chromium 自带的客户端数据库,和你在普通网页里用的完全一样。它的最大优点是零配置、零依赖——只要能在渲染进程里跑 JavaScript,就能直接使用 IndexedDB。
// 在渲染进程中直接使用 IndexedDB
const request = indexedDB.open('MyAppDB', 1);
request.onsuccess = (event) => {
const db = event.target.result;
// 读取、写入数据...
};
适合的场景
- 需要在渲染进程中独立管理的数据,比如用户偏好设置、UI 状态快照、已缓存的列表数据。
- 数据量不大(单条记录几千条到几万条),查询以主键或简单索引为主。
- 不想引入任何额外的原生模块,希望 Electron 应用像网页一样轻便。
需要注意的限制
- 存储配额受浏览器策略约束:虽然 Electron 通常会放宽限制,但并不是无限空间。当可用磁盘空间紧张时,浏览器可能会清理 IndexedDB 数据。
- 数据仅存在于渲染进程:主进程无法直接访问 IndexedDB。如果后端逻辑(例如系统托盘功能、文件监控)需要读取这些数据,就必须通过 IPC 从渲染进程获取,增加了复杂度。
- 不能跨窗口共享:每个渲染进程的 IndexedDB 是独立的(除非你用
same-origin策略共享同一个数据文件,但在多窗口应用中这通常不是问题)。 - 查询能力有限:不支持复杂的 JOIN、聚合、全文搜索,面对稍复杂的数据关系维护成本会直线上升。
实用建议:如果你只是想把应用的一些用户设置(比如窗口大小、主题偏好)或者最近打开的文件列表存起来,IndexedDB 是成本最低的选择。你也可以直接使用 localStorage,但它上限更小(通常约 5-10MB),且只能存储字符串。
12.5.2 本地文件存储:最直接的持久化方式
用 Node.js 的 fs 模块把数据写入一个 JSON 文件,是最原始也最透明的存储方式。很多小工具应用至今仍在使用这种方式来保存配置。
// 主进程中读写 JSON 文件
const fs = require('fs');
const path = require('path');
const userDataPath = app.getPath('userData');
function saveData(data) {
const filePath = path.join(userDataPath, 'data.json');
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
function loadData() {
const filePath = path.join(userDataPath, 'data.json');
if (fs.existsSync(filePath)) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
return {};
}
为了更安全地处理并发写入,通常建议使用辅助库,比如 lowdb(一个基于 Lodash 的 JSON 文件数据库)或 electron-store(专为 Electron 设计的键值对存储,内部也是写 JSON 文件)。
// electron-store 示例
const Store = require('electron-store');
const store = new Store();
store.set('unicorn', '🦄');
console.log(store.get('unicorn')); // 🦄
适合的场景
- 配置文件、用户偏好、应用状态等写入频率极低、数据量很小的信息。
- 数据需要以纯文本形式存在,方便用户手动编辑或备份(例如 Markdown 编辑器的本地笔记,本就是文件形式)。
- 项目初期快速实现原型,暂时不需要复杂的数据库功能。
需要注意的限制
- 查询全靠全量加载:每次读取都需要把整个文件解析成对象,数据量一大(比如几千条记录)性能就会明显下降。
- 并发写入风险:如果你的应用有多窗口同时写入,原生的
fs.writeFile没有锁机制,可能出现数据错乱。electron-store和lowdb对此做了一定程度的保护,但本质上仍然是文件层面的操作。 - 不适合频繁更新的数据:例如实时日志、聊天记录等高频写入场景,文件 I/O 会成为瓶颈。
实用建议:electron-store 几乎是每个 Electron 项目的标配,用来保存用户设置和运行时状态。如果你的应用本身就是围绕“文件”来设计的(比如文本编辑器、笔记工具),那么本地文件 + JSON 的组合也能满足核心需求。
12.5.3 SQLite:真正的嵌入式关系型数据库
当数据量达到数万条、查询开始涉及多表关联、或者你需要一个稳定的事务保障时,就该 SQLite 出场了。SQLite 是一个自包含、零配置的 SQL 数据库引擎,以单个文件的形式存储所有数据,非常适合作为桌面应用的本地数据库。
在 Electron 中集成 SQLite 的主流方式是使用 better-sqlite3 —— 一个同步 API 的 Node.js 原生模块,性能极佳且易于使用。
// 主进程中初始化 better-sqlite3
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.join(app.getPath('userData'), 'myapp.db');
const db = new Database(dbPath);
// 创建表和索引
db.exec(`
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 插入数据(使用预编译语句,防止 SQL 注入)
const insert = db.prepare('INSERT INTO projects (name) VALUES (?)');
insert.run('我的新项目');
// 复杂查询
const projects = db.prepare('SELECT * FROM projects ORDER BY created_at DESC').all();
为什么是 better-sqlite3?
- 同步 API 在 Electron 主进程中反而是优势。主进程的事件循环不会因为同步调用而阻塞用户界面(界面在渲染进程),却能让代码逻辑变得非常清晰——没有回调和 promise 链。
- 性能极佳:相比异步的
sqlite3,better-sqlite3的执行速度通常快 2-5 倍。 - 内置事务:支持原子性的批量操作,保证数据一致性。
适合的场景
- 数据量大(万级到百万级),需要高效的索引查询和全文搜索。
- 数据之间存在关联关系(用户、订单、产品等),需要 JOIN 或多表操作。
- 需要事务支持来保证数据完整性,比如财务记录、任务同步等。
- 想要将数据作为一个完整的数据库文件输出给用户备份或迁移。
需要注意的问题
- 必须要编译原生模块:
better-sqlite3依赖 C++ 代码编译,你需要确保开发环境中安装了 Python 和 C++ 编译工具(Windows 上的 Visual Studio Build Tools,macOS 上的 Xcode Command Line Tools)。在打包分发时,通常使用electron-rebuild或electron-builder的npmRebuild选项来确保模块与 Electron 的 Node.js 版本匹配。这确实是新手遇到最多的坑,不过配置一次后就能稳定运行。 - 数据库操作必须在主进程:出于安全考虑,数据库连接和查询逻辑应该全部放在主进程中,渲染进程通过 IPC 发起请求。千万不要把
better-sqlite3直接暴露给渲染进程。 - 不能直接在渲染进程用:虽然你可以开启
nodeIntegration强行在渲染进程里运行原生模块,但这会带来严重的安全风险,且在新版 Electron 中已经被默认禁止。
安全封装示例(基于 contextBridge):
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('db', {
getProjects: () => ipcRenderer.invoke('db:getProjects'),
addProject: (name) => ipcRenderer.invoke('db:addProject', name),
});
// main.js(主进程处理)
ipcMain.handle('db:getProjects', () => {
return db.prepare('SELECT * FROM projects ORDER BY created_at DESC').all();
});
ipcMain.handle('db:addProject', (event, name) => {
const insert = db.prepare('INSERT INTO projects (name) VALUES (?)');
return insert.run(name);
});
12.5.4 选型决策:一图看懂
| 需求场景 | 推荐方案 | 备注 |
|---------|---------|------|
| 用户设置、主题、窗口状态 | electron-store(JSON 文件) | 最简单,几乎零成本 |
| 缓存少量列表数据(最近的文档、历史记录) | IndexedDB | 不需要主进程访问时使用 |
| 笔记类应用的单个文档内容 | 独立文件(Markdown、TXT 等) | 配合 Node.js 的 fs 模块 |
| 复杂业务数据(项目、用户、交易记录) | SQLite (better-sqlite3) | 万级以上数据、关联查询 |
| 大量日志、实时写入应用 | SQLite 或专用日志文件 | SQLite 的写入性能足够,且支持查询 |
| 需要全文搜索的本地知识库 | SQLite + FTS5 扩展 | better-sqlite3 支持扩展加载 |
12.5.5 一个真实案例的组合运用
某款项目管理工具的 Electron 版使用了以下存储组合:
- 全局配置(登录态、主题):
electron-store - 项目内的所有任务、标签、成员数据:SQLite,每个项目对应一个
.db文件 - 任务附带的说明文档:独立的 Markdown 文件,存储在项目文件夹中
- 用户最近查看的 10 个项目:IndexedDB(纯 UI 层面的快捷入口,无需主进程干预)
这种分层设计让每个存储方案都发挥了自己的长处,没有强制用 SQLite 去存几个配置字段,也没有把几千条任务记录塞进一个 JSON 文件里。
总结一句话:小数据、低频写入用 JSON 文件;纯 UI 缓存用 IndexedDB;数据量一大、查询一复杂,果断上 SQLite。安全上永远记住——数据库操作留在主进程,前端只用 ipcRenderer.invoke。