Skip to main content

Electron 如何持久化数据?

· 5 min read

Electron 持久化没有"标准答案",按数据类型选方案,比选库更重要

  1. 写对目录:一律写 app.getPath('userData'),打包后应用目录是只读的。
  2. 配置 / 偏好:用 electron-store,一个 JSON 文件搞定,不用自己 fs
  3. 结构化大数据:上 better-sqlite3,同步 API,比 IndexedDB 省心。
  4. 敏感信息:token / 密码走 safeStorage,落盘前用系统钥匙串加密。
  5. 别踩的坑:renderer 里的 localStorage 会被用户清缓存清掉,不能当数据库。

先想清楚:数据写到哪里

Electron 持久化第一件事不是选库,是选目录。 大部分人第一次做持久化都栽在这——把文件写进应用自己的目录,开发时一切正常,打包成 .app / .exe 后直接报权限错误。

打包后的应用资源目录(app.asar 所在的地方)是只读的,而且升级会被整包覆盖。用户数据必须写到系统给每个应用分配的可写目录:

const { app } = require('electron');

// 这才是该写数据的地方
app.getPath('userData');
// macOS: ~/Library/Application Support/<AppName>
// Windows: %APPDATA%\<AppName>
// Linux: ~/.config/<AppName>

app.getPath() 还有 templogsdownloads 等 key。规律很简单:要留下来的东西写 userData,临时的写 temp

五种方案,按数据类型选

数据类型推荐方案进程说明
配置 / 用户偏好electron-store主进程键值对,JSON 落盘
结构化 / 大数据better-sqlite3主进程关系型,同步查询
敏感凭据safeStorage主进程系统钥匙串加密
页面临时状态IndexedDB渲染进程随窗口,可被清缓存
二进制文件fs + userData主进程图片、缓存、导出文件

核心判断:能放主进程就别放渲染进程。 渲染进程的 Web 存储(localStorage / IndexedDB)虽然写起来最顺手,但它绑定在 Chromium 的用户数据里,用户清缓存、你切换窗口分区(partition)都可能丢,天生不适合当"真正的数据库"。

配置类:electron-store 一把梭

偏好设置、窗口大小、上次打开的路径这类零散配置,别自己 fs.writeFile 拼 JSON,直接上 electron-store。它自动写到 userData,帮你处理原子写入和默认值:

import Store from 'electron-store';

const store = new Store({
defaults: { theme: 'dark', windowBounds: { width: 1200, height: 800 } }
});

store.set('theme', 'light');
store.get('theme'); // 'light'
store.get('windowBounds'); // 有默认值兜底

它内部就是一个 JSON 文件加了点封装。好处是原子写入——先写临时文件再 rename,避免写一半崩溃导致 JSON 损坏。你自己用 fs 很容易漏掉这一步。

结构化数据:better-sqlite3 优于 IndexedDB

一旦数据有关系、需要查询、量级上千条,就该上 SQLite。Electron 里首选 better-sqlite3,它是同步 API——在主进程里不用到处 await,代码干净得多:

import Database from 'better-sqlite3';
import path from 'node:path';
import { app } from 'electron';

const db = new Database(path.join(app.getPath('userData'), 'app.db'));

db.exec(`CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY, title TEXT, updated_at INTEGER
)`);

const insert = db.prepare('INSERT INTO notes (title, updated_at) VALUES (?, ?)');
insert.run('第一条笔记', Date.now());

有人会问:为什么不用渲染进程自带的 IndexedDB?因为 IndexedDB 的 API 又异步又啰嗦,且数据仍然绑在渲染进程的分区里。把 SQLite 放主进程,通过 IPC 暴露查询接口给渲染进程,才是可控、可备份、可迁移的架构。

原生模块要重新编译

better-sqlite3 是原生模块,Electron 的 Node 版本和系统 Node 不同,需要用 electron-rebuild 针对 Electron 的 ABI 重新编译,否则运行时报 NODE_MODULE_VERSION 不匹配。

敏感数据:safeStorage 加密落盘

access token、密码这类东西绝对不能明文写进 JSON 或 SQLite。 Electron 从 v15 起内置了 safeStorage,底层调用操作系统的凭据体系(macOS Keychain、Windows DPAPI、Linux 的 libsecret):

import { safeStorage } from 'electron';
import fs from 'node:fs';

// 加密后再落盘
const encrypted = safeStorage.encryptString(token);
fs.writeFileSync(tokenPath, encrypted);

// 读取时解密
const decrypted = safeStorage.decryptString(fs.readFileSync(tokenPath));

它加密出来的是 Buffer,你仍然自己决定存哪(文件或数据库的 BLOB 字段都行),但密钥由系统托管,换台机器无法解密。用它之前记得判断 safeStorage.isEncryptionAvailable()——某些无钥匙串环境下会不可用。

渲染进程的 Web 存储:能用,但别依赖

localStoragesessionStorageIndexedDB 在 Electron 渲染进程里都能直接用,因为渲染进程本质是个 Chromium。它们适合存"丢了也无所谓"的临时状态:滚动位置、未提交的草稿、UI 折叠状态。

但把它当主数据库有三个硬伤:

  • 用户清缓存、你调 session.clearStorageData() 会一起清掉
  • 绑定在特定 partition,多窗口 / 隐身窗口之间不共享
  • 主进程访问不到,做备份 / 导出得绕一圈

所以标准架构是:主进程持有真实数据(electron-store / SQLite),渲染进程只做展示和临时缓存,两者通过 IPC 通信。

小结

选型其实一句话能记住:配置用 electron-store,结构化数据用 better-sqlite3,密钥用 safeStorage,全部落在 app.getPath('userData')。渲染进程的 Web 存储只放临时状态。把这几条对上,Electron 的数据持久化就没有坑了。

References

  1. Electron app.getPath 文档
  2. Electron safeStorage 文档
  3. electron-store
  4. better-sqlite3