桌面壳:Electron Main、Preload 与 Renderer
一、三层不是形式主义
Folo 的桌面工程把 Electron 拆为:
apps/desktop/
├── layer/main/ # Node/Electron Main
├── layer/renderer/ # React UI,可构建为 Web
├── forge/vite config # 打包与发布
└── e2e/ # Electron/Web Playwright
这种分层的核心不是“文件夹好看”,而是三种安全与部署边界:
- Main 能访问操作系统和 Node;
- Preload 只暴露经过筛选的能力;
- Renderer 尽量像普通 Web 应用一样开发和测试。
二、Main 进程负责“系统事实”
从 layer/main/src 的组织可以看到 Bootstrap、窗口、菜单、日志、环境、更新、系统信息等职责。Main 还依赖 electron-updater、electron-store、electron-log、node-machine-id、字体列表和文件处理库,说明它承担的是“应用容器”而非 Feed 业务。
一个好判断是:如果某段代码需要 window、app lifecycle、native menu、文件系统或自动更新,它属于 Main/Preload;如果只是订阅一个 Feed,它应当停留在共享业务层。
三、Preload 的最小 API
源码暴露:
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
contextBridge.exposeInMainWorld('platform', process.platform)
其中 api 不是“所有 Main 方法”,而是窗口模糊和商店渠道等少量平台事实。真正的系统动作通过已有的 Electron Toolkit API 或专门的 IPC 装饰器完成。
这使 Renderer 可以做到:
- 读取平台能力,而不是猜测操作系统;
- 在 Web 构建里通过共享常量/空实现继续运行;
- 避免把 Node 模块打进用户可注入的页面上下文。
四、Renderer 是 Web 应用,不是“Electron 页面脚本”
Renderer package 的依赖包含 React、React Router、TanStack Query、Zustand、数据库、共享组件、编辑器、阅读器和 PWA 相关工具。它可以使用 pnpm dev:web 启动 Web 模式,也可以由 Electron Vite 构建为桌面渲染器。
这带来一个重要收益:UI 业务的测试可在浏览器/Happy DOM/Playwright 中进行;桌面自动化只需要覆盖窗口、协议、原生菜单、更新和少量集成路径。
五、自动生成路由的工程意义
router.tsx 从 generated-routes 导入页面树。页面目录里的 (main)、(layer)、(subview) 等分组名描述路由组织,不一定出现在 URL 中。
自动生成路由可以减少:
- 手动维护页面注册表;
- 新页面遗漏 loader 或 lazy import;
- 桌面和 Web 两套路由不一致。
但它也要求开发者理解生成规则:调试路由问题时,要同时看页面文件名、生成产物和 router.tsx 的最终包装。
六、桌面特有能力如何影响架构
自动更新
桌面依赖 electron-updater,版本信息来自 release 配置和构建标签。更新不是 UI 的 fetch,而是 Main 进程控制的安装生命周期。
多窗口/协议
Deep link、分享链接和调试代理可能决定窗口打开哪个路由。Renderer 只消费路由结果,协议解析和单实例协调留在 Main。
数据导出
数据库层的 exportDB 由 Renderer 触发,但底层数据访问根据平台实现。导出功能因此能被设置页面复用,而不必知道 IndexedDB VFS 细节。
七、桌面测试的正确分层
建议按风险而不是按目录测试:
- Store/DB:纯单元与集成测试,验证数据事实;
- Renderer:页面、路由、交互和 Web Playwright;
- Electron:窗口、preload、协议、更新和文件能力;
- Release:产物、签名、安装/升级和渠道。
把所有测试都放到 Electron 会慢,把所有测试都放到 DOM 又覆盖不了 Main。Folo 的目录结构给出了分层测试的自然位置。