本地状态与持久化
安排窗口状态、共享事实、数据库、目录和凭据。
本页整合 Electron 归档分稿中的机制、接口和接线。归档使用 Electron 44 系列作为其版本讨论背景;本文接口依据官方文档及 breaking changes 核查,实际采用时仍需固定项目版本。包版本、维护状态和原稿实测均是对应日期的历史信息。本次未启动 Electron、构建安装包、签名、公证或验证更新。代码块按各自标注区分片段与组合示例。
版本背景:截至 2026-10-04,Electron 最新稳定版为 44.x(2026-08-25 发布,Chromium 152 / Node 24),官方支持窗口内为 42/43/44 三个大版本;Electron 40(2026-01-15)起内置 Node 24。本章所有版本号与维护状态均以文末来源为准,无法核实的标注「待核实」。
3.1 原理与心智模型:四层状态
一个典型的 Electron 桌面应用,本地状态可以拆成四层,每层的读写路径、生命周期与选型逻辑完全不同:
- 渲染进程 UI 状态:视图状态(选中项、面板开合、表单草稿),生命周期跟随窗口,单窗口内闭合,用 React/Vue 生态的状态库管理。
- 主进程应用状态:窗口注册表、托盘、全局快捷键、原生服务(更新器、网络监测)、权限缓存。这是整个应用的「单一事实来源(Single Source of Truth)」,多窗口共享,通过 IPC 推送/拉取。
- 持久化数据:配置项、用户内容、缓存。落在
app.getPath('userData')下的文件或数据库,跨启动存活。 - 敏感凭据:token、密码、私钥。永远不与普通数据混放,走
safeStorage+ 系统钥匙串。
这个分层的核心动机来自 Electron 的进程模型:主进程(main)是常驻的 Node.js 进程,持有全部原生能力;每个 BrowserWindow 是独立的沙箱化 Chromium 渲染进程,默认 nodeIntegration: false、sandbox: true(自 Electron 20 起 preload 也被沙箱化)。因此渲染进程天然接触不到文件系统与原生 API,任何持久化与原生状态都必须经由 IPC——这是后面所有选型的前提。
3.2 渲染进程 UI 状态库选型
3.2.1 候选库现状(2026-10-04 查询 npm registry)
| 库 | 最新版 | 发布日期 | 模型 | 适配框架 |
|---|---|---|---|---|
| Zustand | 5.0.15 | 2026-08-13 | 不可变 store + hooks,无 Provider | React(也可 vanilla) |
| Redux Toolkit (RTK) | 2.13.0 | 2026-09-29 | action/reducer + Immer + RTK Query | React 为主 |
| Valtio | 2.3.2 | 2026-05-01 | Proxy 可变快照 | React |
| Jotai | 3.0.1 | 2026-09-29 | 原子化(atom)自底向上 | React |
| Pinia | 4.0.3 | 2026-08-12 | store 组合式 API | Vue |
社区调查(State of React / State of JS 2025 的二手转述)显示 Zustand 在「留存率/兴趣度」上居首,Redux Toolkit 在「使用量」上居首;该结论来自二手汇总,待核实(见参考来源 tech-insider 一文)。
3.2.2 桌面场景与 Web 的差异
桌面应用没有 SSR/hydration 问题,但有几个 Web 上不突出的取舍点:
- 持久化默认落点变了。Zustand
persist中间件默认写入localStorage,而 Electron 里 localStorage 属于 Chromium session 存储,落在sessionData(默认即userData)目录,且跟随 partition 隔离。若用户清理缓存或你重定向了sessionData,UI 状态会一起丢——重要的用户数据不应只存 localStorage。 - 单窗口单实例 store 就够了。桌面没有多标签页共享需求,但有多窗口需求:每个窗口加载各自的 bundle,store 互不共享,跨窗口同步必须回到主进程(见 3.3)。
- DevTools 时间旅行在桌面调试中价值很高。RTK + Redux DevTools 有完整 action 时间线;Zustand 的
devtools中间件可用但默认没有具名 action,历史记录较薄(二手结论,见 theroadtoenterprise 对比文)。 - RTK Query 在桌面仍有意义:对接后端 API、缓存与轮询逻辑开箱即用;纯本地应用(Zustand/Valtio/Jotai)则用不上这块,选 RTK 的收益下降。
选型建议(2026):新项目、中小型桌面应用,React 技术栈首选 Zustand(轻、无 Provider、persist 中间件内置版本化迁移);大型团队、已有 Redux 经验或重度依赖 RTK Query/严格 action 审计,继续 RTK;Vue 项目用 Pinia(持久化配 pinia-plugin-persistedstate,最新 4.7.1);Valtio/Jotai 适合喜欢可变代理/原子粒度模型的团队,与桌面特性无特殊关联。
3.2.3 关键示例:Zustand persist 带版本迁移
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface UiState {
sidebarOpen: boolean;
lastWorkspacePath?: string;
setSidebar: (open: boolean) => void;
}
export const useUiStore = create<UiState>()(
persist(
(set) => ({
sidebarOpen: true,
setSidebar: (sidebarOpen) => set({ sidebarOpen }),
}),
{
name: 'app-ui', // storage key,必须唯一
storage: createJSONStorage(() => localStorage),
// 只持久化需要的字段,函数/DOM 引用一律排除
partialize: (s) => ({ sidebarOpen: s.sidebarOpen, lastWorkspacePath: s.lastWorkspacePath }),
version: 2, // 持久化 schema 版本号
migrate: (persisted, version) => {
// version 不匹配时才触发;旧版本逐级升级
if (version === 1) {
return { ...(persisted as object), lastWorkspacePath: undefined };
}
return persisted as UiState;
},
},
),
);要点:version 记录在持久化数据里,与当前不匹配时旧数据默认不直接使用,migrate(persistedState, version) 负责迁移——这是官方文档明确的行为。注意 migrate 只在版本不匹配时执行,首次安装(无历史数据)不会调用。
3.3 主进程状态:单一事实来源与多窗口广播
3.3.1 原理
窗口管理、托盘、全局快捷键、原生功能只能活在主进程。官方 IPC 教程给出的通信模式是:
- renderer → main:
ipcRenderer.invoke+ipcMain.handle(两向、返回 Promise,官方推荐);避免sendSync(同步阻塞渲染进程直至返回)。 - main → renderer:
webContents.send+ipcRenderer.on;没有 main→renderer 的invoke等价物,回执需 renderer 另行send。 - renderer ↔ renderer:没有直连通道,要么以主进程为消息代理,要么由主进程通过
MessageChannelMain把一对MessagePortMain分别交给两个窗口,建立直连。
因此多窗口同步的惯用法是:主进程持有 canonical state + 事件广播。所有变更必须先经主进程写入,再由主进程扇出(fan-out)给所有窗口;禁止窗口 A 直接 invoke 改完自己改内存、指望窗口 B 「以后拉取」——那会立刻出现两份事实。
// main.ts —— 极简的主进程状态总线
import { BrowserWindow, ipcMain } from 'electron';
class AppState {
private windows = new Map<string, BrowserWindow>();
private state = { online: true, activeProject: null as string | null };
register(id: string, win: BrowserWindow) {
this.windows.set(id, win);
win.on('closed', () => this.windows.delete(id));
}
private broadcast(event: string, payload: unknown, exceptWebContentsId?: number) {
for (const win of BrowserWindow.getAllWindows()) {
if (win.isDestroyed()) continue;
if (win.webContents.id === exceptWebContentsId) continue; // 可选:跳过发起方
win.webContents.send(event, payload);
}
}
patch(patch: Partial<AppState['state']>, originWebContentsId?: number) {
this.state = { ...this.state, ...patch };
this.broadcast('app:state-changed', this.state, originWebContentsId);
}
snapshot() { return this.state; }
}
export const appState = new AppState();
ipcMain.handle('app:get-state', () => appState.snapshot());
ipcMain.on('app:patch-state', (event, patch) => appState.patch(patch, event.sender.id));// preload.ts —— contextBridge 只暴露最小 API,不透传 ipcRenderer
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('appApi', {
getState: () => ipcRenderer.invoke('app:get-state'),
patchState: (patch: unknown) => ipcRenderer.send('app:patch-state', patch),
// 官方文档明确警告:不要把 callback 原样传给 ipcRenderer.on,
// 会通过 event.sender 泄漏 ipcRenderer;必须包装并剥离 event
onStateChanged: (cb: (s: unknown) => void) => {
const listener = (_e: Electron.IpcRendererEvent, s: unknown) => cb(s);
ipcRenderer.on('app:state-changed', listener);
return () => ipcRenderer.removeListener('app:state-changed', listener);
},
});渲染侧把 onStateChanged 的推送接到 Zustand/RTK 里,即形成「主进程是唯一写入点,渲染层是投影」的单向数据流。IPC 只能传结构化克隆可序列化的数据(Element、BrowserWindow 等 DOM/原生对象不可传),错误经 handle 抛出时也只有 message 到达渲染端——这两点都是官方文档明示的约束。
3.3.2 重状态:utilityProcess 隔离
大量数据(如全文索引、加密、图片处理)不建议直接放主进程——主进程被阻塞会卡住所有窗口的 IPC 与窗口管理。官方 utilityProcess 模块等价于 Node 的 child_process.fork(基于 Chromium Services API),只能在主进程 fork,但可以把一个 MessagePortMain 转移给 webContents,让渲染进程与该子进程直连而不经过主进程中转。把 SQLite 等重 I/O 放进 utility process 是 2025-2026 年逐渐流行的做法(配套 better-sqlite3 的 worker 线程支持)。
3.3.3 单实例锁:多实例并发写的第一道防线
app.requestSingleInstanceLock() 返回 boolean:拿到锁的实例继续运行,未拿到的实例应立即退出,其启动参数通过主实例的 second-instance 事件送达,签名为 (event, argv, workingDirectory, additionalData)。两个细节(均出自官方文档):argv 可能被 Chromium 追加/重排 flag(如 --original-process-start-time),精确参数应走 additionalData;macOS/Linux 上该消息上限 32MB,超限直接丢弃且不触发事件。对绝大多数桌面应用,拿到锁 + 单实例是避免「两个进程同时写同一个配置文件/数据库」的最简单解法。
3.4 本地持久化方案对比
3.4.1 选型总表
| 方案 | 运行位置 | 原生依赖 | 适用规模 | 多实例并发写 | 2026 维护状态 |
|---|---|---|---|---|---|
| electron-store / conf(JSON) | main | 无 | KB 级配置 | 弱(整文件覆盖写) | v11.0.2(2025-10),ESM-only,要求 Electron 30+ |
| 自管 JSON 文件 | main | 无 | KB~几 MB | 取决于实现(原子写 + 单写者) | VS Code 自用方案,持续演进 |
| better-sqlite3 | main / utilityProcess | 原生模块(需 rebuild) | GB 级、复杂查询 | WAL 模式下多连接读写尚可 | v13.0.3(2026-08),活跃 |
| node:sqlite(内置) | main | 无(Node 内置) | 中小规模 | 同 SQLite,但 API 同步、实验性 | Node 24 中 Stability: 1.2(实验性) |
| sql.js(WASM) | 任意进程 | 无 | 中小、免编译场景 | 差(需手动导出落盘) | v1.14.2(2026-08) |
| IndexedDB | renderer | 无 | 渲染侧缓存/离线数据 | 随 session 隔离 | Chromium 内置,随版本更新 |
| LevelDB(level) | main / utilityProcess | 原生(可配 pure JS) | 键值型中等数据 | 单进程独占 | v10.0.0(2025-04) |
3.4.2 JSON 配置:electron-store 与自管文件
electron-store 仍是「小配置」的默认答案,但 2026 年使用它必须知道三件事(均出自其 README):
- 只在主进程用。README 明确:由于 Electron 默认禁用 renderer 的 Node 集成、自 Electron 20 起 preload 也被沙箱化,在 renderer/preload 引入会报
Can't resolve 'fs';官方建议「keep the store in the main process and expose it over IPC」(ipcMain.handle+ipcRenderer.invoke)。 - 它不是数据库:「This package is not a database. It simply uses a JSON file that is read/written on every change」——每次变更全量读写
userData/config.json,只适合用户设置、轻量缓存;大 blob 应存盘后在 store 里存路径。 - 包是 native ESM,不再提供 CommonJS 导出;v11.0.2 要求 Electron 30+。
其底层 conf(v15.1.0)的 README 承诺「Changes are written to disk atomically, so if the process crashes during a write, it will not corrupt the existing config」——即写坏的是临时文件,旧配置仍在。
自管 JSON 的标杆是 VS Code。其主进程 StateService(src/vs/platform/state/node/stateService.ts,本次调研直接读取了该源码)的 FileStorage 类做对了几件小事,值得照抄:
- 节流写入:
ThrottledDelayer把保存合并到 100ms 窗口内,高频setItem不会打爆磁盘; - 内容比对跳过无效写:序列化结果与
lastSavedStorageContents相同则直接返回; - 原子写:
writeFile(..., { atomic: { postfix: '.vsctmp' } }),先写临时文件再改名; - 优雅关闭:
close()以 0 延迟 flush 挂起的写入,保证退出前落盘; - 初始化未完成前不写(避免把空 state 覆盖到磁盘上)。
// 自管 JSON 的最小可靠实现(节选自 VS Code FileStorage 的思路)
private async doSave(): Promise<void> {
await this.initializing; // 等 init 完成,防止覆盖
const serialized = JSON.stringify(this.storage, null, 4);
if (serialized === this.lastSavedStorageContents) return; // 内容没变不写
await this.fileService.writeFile(this.storagePath,
VSBuffer.fromString(serialized),
{ atomic: { postfix: '.vsctmp' } }); // 临时文件 + rename
this.lastSavedStorageContents = serialized;
}3.4.3 SQLite:better-sqlite3、node:sqlite、sql.js
用户内容(聊天记录、文档库、索引)超过几十 KB 就应升级到 SQLite:
- better-sqlite3(v13.0.3,2026-08-05 活跃维护):同步 API、事务支持、官方 README 称其「much faster than node-sqlite3 in most cases」并支持 worker 线程。代价是原生模块:Electron 内置 Node 的 ABI 与系统 Node 不同,必须用
@electron/rebuild或electron-builder install-app-deps为 Electron 版本重编/下载对应 prebuilt;忘记 rebuild 的典型报错是NODE_MODULE_VERSION 115 ... requires NODE_MODULE_VERSION 118(WiseLibs issue #1111 原文)。CI 与本地每次npm install后都要保证 rebuild 脚本执行。 - node:sqlite(Node 内置,
DatabaseSync):无原生依赖、无需 rebuild 是它对 Electron 最大的吸引力。Electron 40+ 内置 Node 24,可直接import { DatabaseSync } from 'node:sqlite'。但 Node 24 官方文档将其标为 Stability: 1.2(实验性),且目前只有同步 API;用 Vite 打包主进程时还需把它标记为 external,否则会报 "Module 'node:sqlite' has been externalized for browser compatibility"(vitejs/discussions/19278)。生产采用前需自评实验性风险。 - sql.js(v1.14.2,2026-08-14):SQLite 的 WASM 编译,零原生依赖、任意进程可跑,但数据库在内存里,持久化要自己
export()二进制再写盘——适合只读分析、嵌入式场景,不适合频繁写入的主存储。
schema 迁移:SQLite 惯用 PRAGMA user_version 记录 schema 版本,启动时逐版本执行迁移脚本并递增;配合事务保证迁移原子性。Zustand persist 侧则用 version + migrate(见 3.2.3)。electron-store 自身不带迁移器,惯例是在文件里放一个 schemaVersion 字段手写升级链。
多实例并发写:JSON 文件没有并发控制,两个进程同时写就是 last-writer-wins,原子写只能防「写坏」防不了「写丢」——所以要么单实例锁(3.3.3),要么把文件的所有权收归一个进程(主进程或 utility process),其他进程经 IPC 访问。SQLite 侧开启 PRAGMA journal_mode = WAL 可让读写并发,但跨进程多连接仍会遇到 SQLITE_BUSY,需要 busy timeout 与重试;经验上**「单写者进程 + IPC/MessagePort 服务化」比「多连接并发写」省心得多**(工程惯例总结,非官方文档结论)。
3.4.4 IndexedDB 与 LevelDB
- IndexedDB 属于 Chromium 的 session 存储,跟随
sessionData目录(默认即 userData)与 partition(persist:前缀持久化,否则 in-memory,getStoragePath()返回 null)。优点是渲染进程原生可用、异步、容量大;缺点是主进程读不到、清缓存/换 partition 即丢、Chromium 升级偶发损坏。合理定位:渲染侧缓存与离线数据,不是应用事实来源。 - LevelDB(npm
level,v10.0.0,2025-04)是经典嵌入式 KV,Chrome 自家的 IndexedDB 底层即基于 LevelDB 系。JS 侧level包 API 友好,但同样以独占锁方式打开数据库,多进程共享需要单写者模式;社区活跃度低于 SQLite 方案,新项目若无既有生态包袱,一般直接选 SQLite。
3.4.5 数据目录规范
官方文档 app.getPath('userData') 定义为「appData 加应用名」:macOS 是 ~/Library/Application Support/<App>,Windows 是 %APPDATA%\<App>,Linux 是 $XDG_CONFIG_HOME(或 ~/.config)/<App>。两点官方建议值得写进规范:
- 应用自己的文件放 userData 的子目录(如
path.join(app.getPath('userData'), 'my-app-data')),避免与 Chromium 自己的子目录(Cache、Local Storage等)命名冲突; sessionData(session 生成的数据:localStorage、cookies、磁盘缓存等)默认指向 userData,若不依赖浏览器存储,可在ready之前用app.setPath('sessionData', ...)挪走,免得 Chromium 巨大的磁盘缓存污染你的数据目录、也让「清缓存不伤配置」成为可能。
3.5 敏感凭据:safeStorage 与系统钥匙串
3.5.1 机制
safeStorage 是 Electron 15(2021-09-21 发布)引入的主进程模块,用操作系统级加密体系加密字符串,磁盘上只落密文。平台后端(官方文档):
- Windows:DPAPI——防同机其他用户,不防同用户的其他应用;
- macOS:Keychain——密钥按应用隔离,防其他用户与其他应用;应用需正确签名,否则 Keychain 授权无法跨更新持久(未签名/ad-hoc 应用会出现 Keychain 查询失败,官方 issue #43233 记录过升级后触发密码弹窗的案例);
- Linux:同步 API 走 kwallet/kwallet5/kwallet6 或 gnome_libsecret;异步 API 优先走
org.freedesktop.portal.Secret(Flatpak/沙箱友好)及 Secret Service。无任何密钥服务时降级为basic_text(内置硬编码明文口令加密,getSelectedStorageBackend()返回basic_text可检测),此时仅是混淆而非保护;可用--password-store启动参数强制指定后端。Linux 上后端探测需等ready事件,之前返回unknown。
官方文档现在的推荐是优先异步 API:isAsyncEncryptionAvailable()、encryptStringAsync()、decryptStringAsync()——非阻塞、支持密钥轮换(shouldReEncrypt 为 true 时应回写重加密)、能优雅处理临时不可用;同步 API「可能在未来的 Electron 版本弃用」。文档仓库的 breaking-changes 页已列出计划:Electron 45 弃用同步三件套(isEncryptionAvailable/encryptString/decryptString),Electron 46 随 Chromium 移除同步 OSCrypt 后端一并移除;旧版 encryptString 加密的数据可直接用 decryptStringAsync 解密(同一套平台密钥库)。截至本文日期 44 为最新稳定版,45/46 尚未发布,该时间线属计划内内容、以官方最终发布为准(标注:依据 docs 仓库 main 分支,待核实)。
// main.ts —— 本分册采用的 safeStorage 用法(异步 + 密钥轮换)
import { safeStorage } from 'electron';
import { writeFile, readFile } from 'node:fs/promises';
export async function saveSecret(plain: string, file: string) {
if (!(await safeStorage.isAsyncEncryptionAvailable())) {
throw new Error('OS-level encryption unavailable'); // 决定降级策略,而非静默明文
}
const encrypted = await safeStorage.encryptStringAsync(plain);
await writeFile(file, encrypted);
}
export async function loadSecret(file: string): Promise<string | null> {
const buf = await readFile(file);
const { result, shouldReEncrypt } = await safeStorage.decryptStringAsync(buf);
if (shouldReEncrypt) {
await writeFile(file, await safeStorage.encryptStringAsync(result)); // 密钥轮换后回写
}
return result;
}3.5.2 keytar 已成历史
上一代方案 node-keytar(atom/node-keytar,直接读写系统钥匙串条目)已停止维护:仓库已归档,npm 最后版本停在 7.9.0(Obsidian 论坛等多个社区在 2023-2025 间确认其 archived 状态并寻找替代)。2026 年的替代品:优先 Electron 内置 safeStorage;确需把凭据作为钥匙串「条目」管理(可被系统凭据管理器查看/删除)的,可考虑 @napi-rs/keyring(需原生模块,Obsidian 社区讨论中提及)。
需要清醒认识的边界:safeStorage 防的是「磁盘上可直接读到明文」。Windows DPAPI 防不了同用户其他进程;有研究者分析其 Windows 实现使用 AES-128-CBC 与固定 IV(二手分析,见 chenguangliang.com 博客),同用户进程 attach 调试器即可在解密瞬间读到明文。高价值凭据仍应考虑 OS keychain 条目级存储或系统级 SSO/生物识别授权,而非仅依赖加密文件。
3.6 常见坑与反模式
- 在 renderer 里 import electron-store / 直接写文件:沙箱下
Can't resolve 'fs';关掉 sandbox/nodeIntegration 去迁就是安全反模式。正解:主进程持有,IPC 暴露。 - preload 暴露整个
ipcRenderer或把回调直接传给ipcRenderer.on:官方文档明确指出后者会经event.sender泄漏 ipcRenderer。用contextBridge暴露最小面。 - 把重要用户数据只存 localStorage/IndexedDB:清缓存即丢,主进程不可见。浏览器存储只做缓存。
- 多窗口各自读写同一 JSON 文件:last-writer-wins 丢数据。收敛到主进程单写者 + 广播。
- 忘记单实例锁:两个实例并发写配置/数据库,偶发损坏极难排查。
- better-sqlite3 不 rebuild 就发版:
NODE_MODULE_VERSION不匹配,装到用户机器上直接崩;把electron-builder install-app-deps或@electron/rebuild挂进 postinstall/CI。 - 写 JSON 不做原子写与节流:崩溃瞬间截断文件;高频状态全量落盘拖慢主进程。
- 忽略
shouldReEncrypt:OS 轮换密钥后旧密文虽可解,但不同步重写会一直走慢路径。 - 依赖同步 safeStorage:官方已预告弃用,新代码一律用
encryptStringAsync/decryptStringAsync。 - 把 schema 迁移写成「if 旧结构存在就删了重建」:桌面用户数据不可再生,迁移必须逐版本、可回滚(至少可备份)。
3.7 采用方式与适用条件清单
- 分层:渲染层 UI 状态(Zustand/RTK/Pinia)→ 主进程单一事实来源 + IPC 广播 → userData 下持久化 → safeStorage 管凭据。
- 配置:electron-store(conf)或照抄 VS Code FileStorage 的「节流 + 内容比对 + 原子写 + 关闭前 flush」四件套;文件放 userData 子目录,sessionData 按需重定向。
- 数据:超过轻量配置即上 SQLite;better-sqlite3(要 rebuild)求稳,node:sqlite(内置、实验性)求免编译,重查询搬进 utilityProcess 单写者进程服务化,WAL + busy timeout 应对并发。
- 同步:
invoke/handle为默认、webContents.send推送、MessageChannelMain建直连、requestSingleInstanceLock保单实例。 - 凭据:
safeStorage异步 API +isAsyncEncryptionAvailable检查 +shouldReEncrypt回写;Linux 检测basic_text;放弃 keytar。 - 每条持久化数据都带
version(JSON 字段 /PRAGMA user_version/ persistversion),迁移链单调向前。
参考来源
- Electron 官方文档 — safeStorage: https://electronjs.org/docs/latest/api/safe-storage
- Electron 官方文档 — app(getPath/userData/sessionData/requestSingleInstanceLock): https://electronjs.org/docs/latest/api/app
- Electron 官方文档 — IPC 教程: https://electronjs.org/docs/latest/tutorial/ipc
- Electron 官方文档 — utilityProcess: https://electronjs.org/docs/latest/api/utility-process
- Electron 官方文档 — Session(partition 持久化): https://electronjs.org/docs/latest/api/session
- Electron 文档镜像 breaking-changes(45/46 safeStorage 弃用/移除计划,未发布版本): https://electron.atomgit.com/breaking-changes.html
- Electron 发布页(版本/Chromium/Node 对应): https://releases.electronjs.org/release ; https://endoflife.date/electron
- Electron 38/39/40 发布博客: https://electronjs.org/blog/electron-38-0 ; https://electronjs.org/blog/electron-39-0 ; https://electronjs.org/blog/electron-40-0
- VS Code 源码 StateService(直接读取,原子写/节流实现): https://github.com/microsoft/vscode/blob/main/src/vs/platform/state/node/stateService.ts
- electron-store README(主进程使用建议、ESM、非数据库定位): https://github.com/sindresorhus/electron-store
- conf README(原子写声明): https://github.com/sindresorhus/conf
- Zustand 官方文档 — persist 中间件: https://zustand.docs.pmnd.rs/reference/middlewares/persist ; https://zustand.docs.pmnd.rs/reference/integrations/persisting-store-data
- Zustand 官方文档 — 与 Redux 对比: https://zustand.docs.pmnd.rs/learn/getting-started/comparison
- better-sqlite3(性能/并发声明): https://github.com/WiseLibs/better-sqlite3 ;NODE_MODULE_VERSION 报错示例: https://github.com/WiseLibs/better-sqlite3/issues/1111
- Node.js 官方文档 — node:sqlite(Stability 1.2): https://nodejs.org/docs/latest-v24.x/api/sqlite.html
- Vite 对 node:sqlite 的 externalize 讨论: https://github.com/vitejs/vite/discussions/19278
- keytar 归档与替代讨论: https://www.npmjs.com/package/keytar ; https://forum.obsidian.md/t/electron-safestorage-available/54844
- safeStorage 引入(Electron 15,2021-09): https://www.infoworld.com/article/2269787/electron-framework-adds-encryption-api.html ; https://freek.dev/2103-replacing-keytar-with-electrons-safestorage-in-ray
- macOS safeStorage Keychain 弹窗 issue: https://github.com/electron/electron/issues/43233
- safeStorage Windows 实现安全分析(二手): https://chenguangliang.com/en/posts/blog169_electron-credential-storage-security
- 状态库生态对比(二手,留存率/使用量数据待核实): https://tech-insider.org/zustand-vs-redux-2026 ; https://theroadtoenterprise.com/blog/zustand-vs-redux-toolkit
- npm registry 版本查询(2026-10-04 本地执行
curl registry.npmjs.org):zustand/valtio/jotai/pinia/@reduxjs/toolkit/better-sqlite3/sql.js/electron-store/conf/level/pinia-plugin-persistedstate/keytar
最后更新于