Electron数据持久化:electron-store、IndexedDB和SQLite怎么选?
Electron 应用要存数据,选择比 Web 前端多——除了浏览器自带的 localStorage 和 IndexedDB,还能直接写文件系统、用 SQLite、或者用专门为 Electron 设计的 electron-store。选错了方案,后期迁移成本很高。这篇文章按场景分类,帮你选最合适的存储方案。
方案选择速查
| 方案 | 数据量 | 查询能力 | 适用场景 |
|---|---|---|---|
| electron-store | < 1MB | 无(JSON 读写) | 用户设置、应用配置 |
| localStorage | < 5MB | 无(KV) | 简单状态、主题偏好 |
| IndexedDB | < 100MB | 索引查询 | 离线数据、缓存 |
| SQLite | 无上限 | 完整 SQL | 结构化数据、历史记录、搜索 |
| 文件系统 | 无上限 | 无 | 日志、导出文件、大文件 |
electron-store:最简单的配置存储
electron-store 是 Electron 生态里用得最多的轻量存储——本质就是把 JSON 文件读写封装了一层,加了 schema 校验、默认值、加密支持。
bashnpm install electron-store
javascriptconst Store = require('electron-store') const store = new Store({ defaults: { windowBounds: { width: 1200, height: 800 }, theme: 'system', recentFiles: [], } }) // 读写 store.set('theme', 'dark') store.get('theme') // 'dark' store.get('windowBounds.width') // 1200(支持点号路径) // 删除 store.delete('theme') store.has('theme') // false // 监听变化 store.onDidChange('theme', (newValue, oldValue) => { console.log(`主题从 ${oldValue} 变为 ${newValue}`) })
数据存在 app.getPath('userData')/config.json,Windows 上是 %AppData%/你的应用名/config.json,macOS 上是 ~/Library/Application Support/你的应用名/config.json。
适合存:用户偏好、窗口位置、最近打开的文件列表。不适合存:聊天记录、操作日志、任何需要条件查询的数据——JSON 文件每次读写都是全量操作,数据多了就慢。
localStorage 和 sessionStorage
Electron 的渲染进程里可以用浏览器的 localStorage,但有坑:
javascript// 渲染进程 localStorage.setItem('key', 'value') localStorage.getItem('key')
坑一:容量限制 5-10MB,存不了多少东西。
坑二:数据绑定在 origin 上。如果你的应用用了自定义协议(app.setAsDefaultProtocol),localStorage 的 origin 可能变化,之前存的数据就找不到了。
坑三:同步 API,数据量大时阻塞渲染线程。
坑四:只能在渲染进程用,主进程访问不了。
建议:只在渲染进程存一些临时状态(如表单草稿),重要数据不要依赖 localStorage。
IndexedDB:浏览器里的结构化存储
IndexedDB 是浏览器原生的 NoSQL 数据库,支持索引和事务,容量比 localStorage 大得多。
javascript// 打开数据库 const request = indexedDB.open('MyAppDB', 1) request.onupgradeneeded = (event) => { const db = event.target.result const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true }) store.createIndex('updatedAt', 'updatedAt', { unique: false }) } request.onsuccess = (event) => { const db = event.target.result // 写入 const tx = db.transaction('notes', 'readwrite') tx.objectStore('notes').add({ title: 'Hello', content: 'World', updatedAt: Date.now() }) // 按索引查询 const idxTx = db.transaction('notes', 'readonly') const index = idxTx.objectStore('notes').index('updatedAt') const range = IDBKeyRange.lowerBound(Date.now() - 86400000) // 最近一天 index.openCursor(range).onsuccess = (e) => { const cursor = e.target.result if (cursor) { console.log(cursor.value) cursor.continue() } } }
IndexedDB 的 API 是回调式的,非常难用。推荐用 idb 这个库封装成 Promise:
bashnpm install idb
javascriptconst { openDB } = require('idb') const db = await openDB('MyAppDB', 1, { upgrade(db) { db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true }) } }) await db.add('notes', { title: 'Hello', content: 'World' }) const allNotes = await db.getAll('notes')
IndexedDB 适合缓存数据(如离线文章列表),但复杂查询能力有限——没有 JOIN,没有聚合函数。数据量超过几百 MB 性能也会下降。
SQLite:需要 SQL 时的选择
Electron 主进程是 Node.js,可以直接用 SQLite。这是存储大量结构化数据的最优方案——完整 SQL 支持、事务、索引、百 GB 级数据都没问题。
bashnpm install better-sqlite3
为什么用 better-sqlite3 而不是 sqlite3?因为 better-sqlite3 是同步 API,不用处理异步回调,性能也更好(C 绑定更高效)。在 Electron 主进程里同步不是问题——主进程本就不应该被数据库操作阻塞。
javascriptconst Database = require('better-sqlite3') const path = require('path') const dbPath = path.join(app.getPath('userData'), 'app.db') const db = new Database(dbPath) // 建表 db.exec(` CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, updated_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(updated_at); `) // 插入(prepared statement,防 SQL 注入) const insert = db.prepare('INSERT INTO notes (title, content, updated_at) VALUES (?, ?, ?)') insert.run('My Note', 'Hello world', Date.now()) // 批量插入(事务) const batchInsert = db.transaction((items) => { for (const item of items) { insert.run(item.title, item.content, Date.now()) } }) batchInsert([ { title: 'Note 1', content: 'A' }, { title: 'Note 2', content: 'B' }, ]) // 查询 const notes = db.prepare('SELECT * FROM notes WHERE updated_at > ? ORDER BY updated_at DESC LIMIT 20').all(Date.now() - 86400000) // 搜索 const results = db.prepare("SELECT * FROM notes WHERE title LIKE ?").all('%keyword%')
better-sqlite3 的 native 模块需要针对 Electron 重新编译:
bashnpm install --save-dev electron-rebuild npx electron-rebuild
或者在 package.json 里配置 postinstall:"postinstall": "electron-rebuild"
文件系统:大文件和日志
直接操作文件适合日志、导出数据、用户文档等场景:
javascriptconst fs = require('fs') const path = require('path') // 日志写入(append) const logPath = path.join(app.getPath('userData'), 'app.log') fs.appendFileSync(logPath, `[${new Date().toISOString()}] Event occurred\n`) // 用户文档目录 const docsPath = app.getPath('documents') fs.writeFileSync(path.join(docsPath, 'export.json'), JSON.stringify(data))
大量日志建议用 electron-log,它自动处理文件轮转和大小限制。
数据迁移策略
应用版本升级时,数据结构可能变化。每种方案的迁移方式不同:
electron-store:在 defaults 里加新字段,旧数据自动补默认值。删字段要手动处理:
javascriptconst store = new Store({ defaults: { /* 新 schema */ } }) // 迁移:删除废弃字段 if (store.has('oldKey')) { store.delete('oldKey') }
SQLite:用版本号控制迁移:
javascriptconst currentVersion = db.pragma('user_version', { simple: true }) if (currentVersion < 1) { db.exec('ALTER TABLE notes ADD COLUMN tags TEXT') db.pragma('user_version = 1') }
IndexedDB:onupgradeneeded 在版本号变化时触发,在这里加新 store 和索引。
安全注意事项
- 加密敏感数据:electron-store 支持
encryptionKey选项,密码等敏感数据不要明文存 JSON - 不要存到代码目录:用
app.getPath('userData')获取系统标准路径,不要写进resources/ - 渲染进程不要直接访问文件系统:通过 IPC 让主进程操作,避免开启
nodeIntegration