运行时拓扑:一次启动到底发生了什么
一、四个主要运行时
Folo 不是“一个 React 应用打包成四份”,而是四种不同的运行时:
| 运行时 | 入口线索 | 主要职责 |
|---|---|---|
| Electron Main | apps/desktop/layer/main/src/index.ts | 生命周期、窗口、更新、菜单、系统能力 |
| Electron/Web Renderer | apps/desktop/layer/renderer/src/main.tsx、router.tsx | 业务 UI、路由、Store、Web/PWA |
| React Native | apps/mobile/src/App.tsx | 移动导航、原生能力、后台任务、推送 |
| Fastify + SSR | apps/ssr/index.ts、src/router/global.ts | HTML 壳、Meta/OG、Hydration、分享页 |
另有两个窄运行时:apps/cli 是 client-sdk 的命令行入口,apps/ota 是 Cloudflare Worker,负责版本、制品与策略分发。
二、Electron 启动链
main/src/index.ts 本身很薄:导入 before-bootstrap、bootstrap,然后调用 BootstrapManager.start()。这是一种有意的“编排入口”:启动顺序集中在 manager,而不是散落在 Electron 事件回调里。
三、Preload 是最小能力桥
layer/main/preload/index.ts 只暴露三类东西:
- Electron Toolkit 提供的
electronAPI; - Folo 自己的
api,例如窗口模糊能力、Windows Store 标记; platform字符串。
当 process.contextIsolated 开启时,使用 contextBridge.exposeInMainWorld;否则才降级到 window 写入。这条路径同时服务安全默认值与兼容调试环境。
关键原则是:Renderer 不拿到任意 ipcRenderer。它只能拿到被命名的能力对象,主进程才能继续控制参数校验和副作用。
四、Renderer 路由如何同时服务 Electron 和 Web
router.tsx 根据 IN_ELECTRON 或 debug proxy 选择 createHashRouter,否则选择 createBrowserRouter:
const routerCreator = IN_ELECTRON || isDebugProxyRuntime
? createHashRouter
: createBrowserRouter
Hash 路由避免 Electron 本地协议或静态资源服务器处理深链时丢失页面;Browser 路由给 Web 部署提供正常 URL。页面树通过 generated-routes 导入,路由由文件结构/构建插件生成,减少手写注册表。
五、SSR 启动链
apps/ssr/index.ts 创建 Fastify,注册 request context 和 middie,然后挂载 OG 路由与 global route。开发态由 Vite 提供模板变换,生产态加载预生成的 index.template。
全局路由的重点不是“服务端渲染所有页面”,而是:
- 读取请求 URL;
- 为分享 Feed、List、User 等资源查询 Meta;
- 注入 title、description、Open Graph 图片、App Link 和 hydration 数据;
- 把同一份页面交给浏览器端 React 接管。
如果 Meta 查询发现资源不存在,NotFoundError 会把响应状态设为 404,并在 HTML 上标记 data-not-found。这让爬虫、分享卡片和客户端路由都能获得一致的失败语义。
六、启动时最容易被忽略的事实
DB 初始化不是 UI 初始化
Store hydrate 需要先完成数据库初始化和迁移。若把它放进任意页面的 useEffect,页面之间会出现“有的读本地、有的读空 store”的竞态。Folo 将 hydration 作为 Store/Provider 层职责,页面消费已经稳定的 selector。
SSR 和客户端使用两套网络时机
SSR 在请求阶段拉取 Meta/页面数据并注入 HTML;客户端在 hydration 后恢复 Query、Store 和认证状态。两者的目标不是共享同一套生命周期,而是共享类型和 URL 语义。
平台壳层决定“能做什么”,业务层决定“做什么”
窗口模糊、系统菜单、原生分享、通知和后台任务属于壳层;订阅、条目、收藏、摘要和翻译属于业务层。读源码时先按这个分法,可以避免把平台 API 误认为产品领域模型。