本地数据存储
本地数据存储 💾
桌面应用与 Web 应用最大的区别之一就是:可以持久化存储大量数据。本章我们系统学习 Electron 中常用的本地存储方案。
存储方案对比 💎
| 方案 | 适用场景 | 容量 | 性能 | 易用性 |
|---|---|---|---|---|
| electron-store | 应用配置、用户偏好 | 几 MB | 高 | ⭐⭐⭐⭐⭐ |
| localStorage | 简单数据 | ~10MB | 高 | ⭐⭐⭐⭐⭐ |
| IndexedDB | 结构化数据 | 几百 MB | 中 | ⭐⭐⭐ |
| SQLite (better-sqlite3) | 大数据、复杂查询 | 几 GB | 高 | ⭐⭐⭐ |
| 文件系统 (fs) | 文件、图片、缓存 | 无限制 | 取决于硬盘 | ⭐⭐ |
一、electron-store(推荐) 💎
electron-store 是 Electron 生态最流行的本地配置存储库,基于 JSON 文件实现,简单易用、类型友好,适合存储应用配置、用户偏好、Token 等。
安装 👻
pnpm add electron-store基本使用 👻
// src/main/store.ts
import Store from 'electron-store'
// 简单的 key-value 存储
const store = new Store()
// 设置值
store.set('theme', 'dark')
store.set('user.token', 'abc123')
// 读取值
const theme = store.get('theme') // 'dark'
const token = store.get('user.token') // 'abc123'
// 读取带默认值
const lang = store.get('lang', 'zh-CN') // 'zh-CN'
// 删除
store.delete('user.token')
// 检查存在
store.has('user.token')
// 清空
store.clear()
// 存储文件路径
// Windows: %APPDATA%/my-electron-app/config.json
// macOS: ~/Library/Application Support/my-electron-app/config.json
// Linux: ~/.config/my-electron-app/config.json类型化存储 👻
通过 TypeScript 泛型获得完整的类型提示:
interface AppConfig {
theme: 'light' | 'dark'
language: 'zh-CN' | 'en-US'
user: {
name: string
token: string
avatar?: string
}
recentFiles: string[]
}
const store = new Store<AppConfig>({
defaults: {
theme: 'dark',
language: 'zh-CN',
user: {
name: '',
token: '',
},
recentFiles: [],
},
})
// 完整类型提示
store.set('theme', 'light') // ✅
store.set('theme', 'invalid') // ❌ 类型错误
store.get('user.name') // 类型为 string通过 IPC 暴露给渲染进程 👻
通常应用配置需要在多个窗口间共享,推荐通过 IPC 暴露给渲染进程:
// src/main/index.ts
import Store from 'electron-store'
interface AppConfig {
theme: 'light' | 'dark'
language: string
recentFiles: string[]
}
const store = new Store<AppConfig>({
defaults: {
theme: 'dark',
language: 'zh-CN',
recentFiles: [],
},
})
ipcMain.handle('store:get', (_, key: keyof AppConfig) => {
return store.get(key)
})
ipcMain.handle('store:set', (_, key: keyof AppConfig, value: any) => {
store.set(key, value)
// 通知所有窗口配置已更新
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send('store:updated', { key, value })
})
})// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('storeApi', {
get: (key: string) => ipcRenderer.invoke('store:get', key),
set: (key: string, value: any) => ipcRenderer.invoke('store:set', key, value),
onUpdated: (callback: (data: { key: string; value: any }) => void) => {
ipcRenderer.on('store:updated', (_, data) => callback(data))
},
})<!-- Vue 组件中使用 -->
<template>
<button @click="toggleTheme">
切换到 {{ theme === 'dark' ? '浅色' : '深色' }} 模式
</button>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const theme = ref<'light' | 'dark'>('dark')
onMounted(async () => {
theme.value = await (window as any).storeApi.get('theme')
})
const toggleTheme = async () => {
const newTheme = theme.value === 'dark' ? 'light' : 'dark'
await (window as any).storeApi.set('theme', newTheme)
theme.value = newTheme
}
</script>加密存储 👻
对于敏感数据(如 token、密码),可以开启加密:
pnpm add electron-store
# 加密需要 safeStorage 或加密密钥import Store from 'electron-store'
import { safeStorage } from 'electron'
// 方式一:使用系统 keychain
const store = new Store({
encryptionKey: safeStorage.encryptString('my-secret-key'),
})
// 方式二:自定义加密密钥
const store = new Store({
encryptionKey: 'my-custom-key',
})二、localStorage 💎
localStorage 同样在 Electron 渲染进程中可用,与 Web 用法完全一致:
// 存储
localStorage.setItem('token', 'abc123')
localStorage.setItem('user', JSON.stringify({ name: '张三' }))
// 读取
const token = localStorage.getItem('token')
const user = JSON.parse(localStorage.getItem('user')!)
// 删除
localStorage.removeItem('user')
// 清空
localStorage.clear()localStorage 的局限
- 容量限制(通常 5-10 MB)
- 只能存字符串
- Electron 中如果想跨窗口共享数据,需要用 IPC + electron-store
三、IndexedDB 💎
IndexedDB 是浏览器内置的事务型数据库,适合存储大量结构化数据。在 Electron 中同样可用,但仅限单窗口内(每个 BrowserWindow 有独立的数据)。
安装 Dexie(推荐封装库) 👻
pnpm add dexie定义数据库 👻
// src/renderer/db.ts
import Dexie, { Table } from 'dexie'
export interface Message {
id?: number
conversationId: string
sender: string
content: string
createdAt: number
}
export class AppDatabase extends Dexie {
messages!: Table<Message, number>
constructor() {
super('MyAppDB')
this.version(1).stores({
messages: '++id,conversationId,createdAt',
})
}
}
export const db = new AppDatabase()使用 👻
// 增
await db.messages.add({
conversationId: 'c1',
sender: '张三',
content: '你好',
createdAt: Date.now(),
})
// 删
await db.messages.delete(1)
// 改
await db.messages.update(1, { content: '更新后的内容' })
// 查
const messages = await db.messages
.where('conversationId')
.equals('c1')
.toArray()
// 复合查询
const recent = await db.messages
.where('createdAt')
.above(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toArray()四、SQLite(better-sqlite3) 💎
对于大量结构化数据 + 复杂查询的场景,推荐使用 SQLite。better-sqlite3 是 Node.js 生态最流行的 SQLite 客户端,同步 API、性能极高。
安装 👻
pnpm add better-sqlite3
pnpm add -D @types/better-sqlite3基本使用 👻
// src/main/db.ts
import Database from 'better-sqlite3'
import { app } from 'electron'
import { join } from 'path'
let db: Database.Database | null = null
export function initDatabase() {
const dbPath = join(app.getPath('userData'), 'app.db')
db = new Database(dbPath)
// 创建表
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
content TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users (id)
);
CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id);
`)
console.log('数据库初始化完成:', dbPath)
return db
}
export function getDatabase() {
if (!db) throw new Error('数据库未初始化')
return db
}// 使用示例
import { getDatabase } from './db'
const db = getDatabase()
// 预编译语句(性能更好)
const insertUser = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
const getUserById = db.prepare('SELECT * FROM users WHERE id = ?')
const getAllUsers = db.prepare('SELECT * FROM users ORDER BY created_at DESC')
// 增
const result = insertUser.run('张三', 'zhangsan@example.com')
console.log('新用户 ID:', result.lastInsertRowid)
// 查
const user = getUserById.get(1)
const allUsers = getAllUsers.all()
// 删
const deleteUser = db.prepare('DELETE FROM users WHERE id = ?')
deleteUser.run(1)
// 改
const updateUser = db.prepare('UPDATE users SET name = ? WHERE id = ?')
updateUser.run('李四', 1)
// 事务
const insertMany = db.transaction((users: Array<{ name: string; email: string }>) => {
for (const u of users) {
insertUser.run(u.name, u.email)
}
})
insertMany([
{ name: '用户1', email: 'u1@example.com' },
{ name: '用户2', email: 'u2@example.com' },
{ name: '用户3', email: 'u3@example.com' },
])性能对比
better-sqlite3 的性能非常夸张:
- 插入 10 万条数据:< 1 秒
- 简单查询:< 1ms
- 复杂 JOIN 查询:< 10ms
对于大多数桌面应用,SQLite 完全够用。
五、文件系统 💎
Electron 主进程拥有完整的 fs 模块访问能力,可用于读写文件、缓存图片等。
文件对话框 👻
import { dialog, ipcMain, BrowserWindow, app } from 'electron'
import { join } from 'path'
import { writeFile, readFile } from 'fs/promises'
// 选择保存路径
ipcMain.handle('file:save-as', async (_, data: { defaultName: string; content: string }) => {
const win = BrowserWindow.getFocusedWindow()!
const result = await dialog.showSaveDialog(win, {
title: '保存文件',
defaultPath: join(app.getPath('documents'), data.defaultName),
filters: [
{ name: '文本文件', extensions: ['txt'] },
{ name: '所有文件', extensions: ['*'] },
],
})
if (result.canceled || !result.filePath) return null
await writeFile(result.filePath, data.content, 'utf-8')
return result.filePath
})
// 选择打开文件
ipcMain.handle('file:open', async () => {
const win = BrowserWindow.getFocusedWindow()!
const result = await dialog.showOpenDialog(win, {
title: '打开文件',
properties: ['openFile'],
filters: [
{ name: 'JSON 文件', extensions: ['json'] },
{ name: '文本文件', extensions: ['txt'] },
],
})
if (result.canceled || !result.filePaths.length) return null
const content = await readFile(result.filePaths[0], 'utf-8')
return { path: result.filePaths[0], content }
})用户数据目录 👻
app.getPath('userData') 返回应用专属的用户数据目录,是存储应用数据最安全的位置:
import { app } from 'electron'
import { join } from 'path'
const userDataPath = app.getPath('userData') // 应用数据目录
const documentsPath = app.getPath('documents') // 用户文档
const downloadsPath = app.getPath('downloads') // 用户下载
const tempPath = app.getPath('temp') // 临时文件
const cachePath = app.getPath('cache') // 缓存目录
// 推荐:所有应用数据都放在 userData 下
const dbPath = join(userDataPath, 'app.db')
const configPath = join(userDataPath, 'config.json')
const cacheDir = join(userDataPath, 'cache')实战案例:消息记录存储 💎
结合 better-sqlite3 实现一个完整的消息记录系统:
// src/main/message-store.ts
import Database from 'better-sqlite3'
import { app } from 'electron'
import { join } from 'path'
interface Message {
id?: number
conversationId: string
senderId: string
content: string
type: 'text' | 'image' | 'file'
status: 'sent' | 'delivered' | 'read'
createdAt: number
}
class MessageStore {
private db: Database.Database
constructor() {
const dbPath = join(app.getPath('userData'), 'messages.db')
this.db = new Database(dbPath)
this.init()
}
private init() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT NOT NULL,
sender_id TEXT NOT NULL,
content TEXT NOT NULL,
type TEXT DEFAULT 'text',
status TEXT DEFAULT 'sent',
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_conv_time
ON messages(conversation_id, created_at);
`)
}
add(msg: Omit<Message, 'id'>): Message {
const stmt = this.db.prepare(`
INSERT INTO messages
(conversation_id, sender_id, content, type, status, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`)
const result = stmt.run(
msg.conversationId,
msg.senderId,
msg.content,
msg.type,
msg.status,
msg.createdAt
)
return { ...msg, id: result.lastInsertRowid as number }
}
listByConversation(
conversationId: string,
options: { limit?: number; before?: number } = {}
): Message[] {
const { limit = 50, before = Date.now() } = options
const rows = this.db
.prepare(`
SELECT * FROM messages
WHERE conversation_id = ? AND created_at < ?
ORDER BY created_at DESC
LIMIT ?
`)
.all(conversationId, before, limit)
return rows.reverse() as Message[]
}
search(keyword: string): Message[] {
return this.db
.prepare(`SELECT * FROM messages WHERE content LIKE ? ORDER BY created_at DESC LIMIT 100`)
.all(`%${keyword}%`) as Message[]
}
count(): number {
const row = this.db.prepare('SELECT COUNT(*) as total FROM messages').get() as { total: number }
return row.total
}
close() {
this.db.close()
}
}
export default new MessageStore()小结
本章我们学习了 Electron 中常用的本地存储方案:
- electron-store:配置、用户偏好(推荐)
- localStorage:简单数据
- IndexedDB:结构化数据(单窗口)
- SQLite (better-sqlite3):大数据、复杂查询
- 文件系统:文件、图片、缓存
选择哪种方案取决于数据量、查询复杂度、跨窗口需求。
下一章我们将学习 打包与发布。
至此,本章节的学习就到此结束了,如有疑惑,可对接技术客服进行相关咨询。