桌面端需要在用户选择工作区后,重启应用时自动恢复上次打开的工作区。这个需求表面上是“存在哪里”,实际要验证的是完整链路:选择工作区后是否写入、下次启动时是否读到、读到后是否真正进入页面状态。
问题背景
现象是用户选择工作区、关闭应用、重新打开后,页面仍然显示“未选择工作区”。这类问题首先要确认三件事:
- 当前运行的调试进程是不是使用同一个 Electron
userData目录。 - 渲染进程加载的是不是最新的 Vite 模块。
- 代码到底把“上次工作区”写到了哪里。
一次实际排查中,运行进程使用的是下面这类目录:
C:\Users\WINDOWS_USER\AppData\Roaming\DESKTOP_APP开发服务器也能返回最新的目标页面模块。这说明问题不能简单归因于“跑的是旧应用”;但这类证据只能排除一部分可能性,不能证明持久化和恢复链路已经执行。
根因
这次最初的结论写得不准确:问题不能归结为“没有写 localStorage”。如果在 Electron userData 目录写入一个 JSON,并且启动时能通过 IPC 读回,再把读到的路径应用到 workflowState.workspace,功能同样应该成立。
真正的问题是当时没有完成端到端证据验证。我把“应该使用 localStorage”和“JSON 方案为什么没生效”混成了一个结论。正确的排错应该把链路拆开验证:
- 选择工作区后,偏好文件或
localStorage是否真实写入。 - 重启后,恢复逻辑是否真实执行。
- 恢复逻辑读到的路径是什么。
- 恢复函数是否成功加载工作区状态并更新页面状态。
- 如果失败,错误是否展示在“未选择工作区”页面,而不是只写进日志。
当时错误实现创建了:
workspace-preferences.json这个 JSON 方案本身不是不能工作。它的问题在于实现增加了主进程 IPC、preload 暴露、授权检查、异步恢复、页面状态应用多个环节,但当时只验证了工具函数测试通过,没有用运行时证据证明每一层都通。用户看到重启后仍是“未选择工作区”,说明断点一定发生在“读取 JSON 后应用到页面状态”这条链路上,而不是“存 JSON 这种方式天然不行”。
最终采用 localStorage,是因为这类非敏感、仅供当前页面恢复使用的偏好可以直接在渲染进程内读写,从而缩短链路:
const lastWorkspaceDirStorageKey = "desktop-app.last-workspace-dir.v1";
function loadLocalLastWorkspaceDir(): string | null {
if (typeof window === "undefined") return null;
return window.localStorage.getItem(lastWorkspaceDirStorageKey);
}
function saveLocalLastWorkspaceDir(workspaceDir: string): void {
if (typeof window === "undefined") return;
window.localStorage.setItem(lastWorkspaceDirStorageKey, workspaceDir);
}Electron 会通过 Chromium session 持久化 localStorage。其 sessionData 路径默认与 userData 相同,磁盘上通常能看到类似下面的目录:
C:\Users\WINDOWS_USER\AppData\Roaming\DESKTOP_APP\Local Storage\leveldbLocal Storage\leveldb 是 Chromium 当前的内部存储布局,不是应用可以依赖的稳定 API。应用应通过 window.localStorage 访问数据,通过 app.getPath("userData") 和 app.getPath("sessionData") 确认运行时路径,不应直接读写 LevelDB 文件。
选择 localStorage 后不再需要额外的 workspace-preferences.json。如果以后重新采用 JSON 方案,必须用运行时验证或测试覆盖完整链路,而不是只测文件读写函数。
不要把 profile 目录结构当成应用接口
Electron 的 userData 适合存放应用配置,sessionData 存放 Chromium session 产生的 localStorage、cookie、磁盘缓存和网络状态。sessionData 默认指向 userData,但应用可以在 ready 事件前覆盖它,因此两者不能永远画等号。
排查时可以观察 profile 目录辅助判断,但其中大部分子目录属于 Chromium 实现细节,名称和布局可能随版本变化。常见内容包括:
Local Storage:Chromium Web Storage 的内部持久化数据,不应由应用直接修改。Session Storage:渲染进程sessionStorage,适合临时状态,不适合跨重启恢复工作区。APP_DATA_SUBDIR:应用自行管理的配置或业务状态,应使用明确的子目录和文件名。Cache、Code Cache、GPUCache、DawnGraphiteCache、DawnWebGPUCache:Chromium 资源、JS/WASM 编译、GPU/WebGPU 缓存,可再生成。Network:Chromium 网络 profile 数据,包括 cookies、trust tokens 和网络持久状态。blob_storage:浏览器 Blob API 的存储目录。Dictionaries:Chromium 拼写检查词典。Shared Dictionary、SharedStorage:Chromium 的共享字典压缩缓存和 Shared Storage 数据库。Local State、Preferences等:Chromium/Electron profile 级别状态和偏好,不是应用手写的业务配置。
还要验证 origin 和 session partition
即使 profile 路径相同,localStorage 也不会自动跨 origin 或 session partition 共享。开发环境从 http://localhost:PORT 加载、生产环境从自定义协议或本地文件加载时,可能对应不同的存储空间;使用 persist:NAME partition 的窗口,也可能与默认 session 分离。
因此完整排查至少要记录:
- 当前页面的
location.origin。 BrowserWindow使用的 partition,是否带有persist:前缀。- main 进程返回的
app.getPath("userData")和app.getPath("sessionData")。 - 写入和恢复发生时使用的 storage key 与实际值。
经验
遇到 Electron “重启后没有恢复”的问题,不能只争论存储介质。无论使用 localStorage、JSON 文件还是数据库,都要验证同一条链路:写入成功、下次启动读取成功、读取结果进入 React 状态、页面没有被空状态提前挡住、失败原因能显示出来。
如果需求明确是 localStorage,可以让渲染进程直接读写 window.localStorage,并把它用于非敏感、与当前页面 origin 和 session 绑定的偏好。授权 token、原生能力配置、需要主进程权限或加密保护的数据,则更适合由主进程管理。工作区路径本身也可能暴露用户名、客户名或项目名,不应把 localStorage 当成秘密存储。
验证时不要只看源码,还要确认:
Get-ChildItem -Force "$env:APPDATA\DESKTOP_APP"同时应从 main 进程打印稳定 API 返回的实际路径:
console.log({
sessionData: app.getPath("sessionData"),
userData: app.getPath("userData"),
});如果启动参数或测试工具覆盖了 Chromium profile,也要核对实际使用的 user data directory。应用自身需要定制目录时,优先在 ready 之前使用 app.setPath(),不要把内部子目录结构写死在业务逻辑中。这样才能区分“代码没生效”“origin 或 partition 变化”“跑错 profile”和“存储位置理解错误”几类问题。