网络 API 与 SSR:同一套协议的两个消费面
一、client-sdk 是远端协议的稳定入口
Folo 在多个应用中依赖 @follow-app/client-sdk:桌面 Renderer、移动端、SSR、CLI、OTA 相关逻辑都能看到它的类型或 API。它把服务端路由、Schema、错误和 API Client 封装在一个协议包里。
本地 Store 不直接把 SDK 返回对象往页面传,而是经过 APIMorph。SSR 的 Query 则可以直接使用 apiClient.api.feeds.get、lists.get 等协议方法,因为 SSR 的职责是根据 URL 渲染资源,不需要把所有数据持久化到本地 SQLite。
二、Web/SSR 的双层应用
apps/ssr 同时包含:
- Fastify 服务端入口:
index.ts、src/router/global.ts; - React 客户端:
client/App.tsx、client/router.tsx、client/pages; - Vite/tsdown/Worker/Vercel 构建配置;
- Meta、OG、hydration 和错误处理。
这个组合让分享页具有服务端首屏和 SEO 能力,但它也要求团队明确“服务器代码”和“浏览器代码”的 import 边界。
三、请求到分享页的完整链路
四、Meta 注入为什么要单独一层
src/router/global.ts 的 safeInjectMetaToTemplate 负责把异常分类:
- 资源不存在 → 404,并标记
data-not-found; - API FetchError 有状态码 → 保留远端状态;
- MetaError → 让上层处理明确的错误;
- 其他异常 → 记录日志但尽量返回可用模板。
真正的 injectMetaToTemplate 再按类型写入:Open Graph、普通 meta、title、description、hydrate。这种分层让路由控制 HTTP 结果,Meta Handler 控制“需要注入什么”,SEO builder 控制标签结构。
五、Hydration 数据的用途
分享页在服务端已经查到 Feed/List 数据,不应让浏览器加载完 HTML 后立刻重复请求同样数据。Meta 层注入 hydrate payload,客户端 Query/Provider 可以用它作为初始缓存。
这类数据有严格边界:只能注入页面需要的公开信息,不能把认证 Cookie、私密订阅或服务端秘密放进 HTML。global.d.ts 中对 window.__HYDRATE__ 的说明也说明它是面向公开查询的客户端预热层。
六、Web 路由与 Electron 路由的差别
桌面 Renderer 根据 IN_ELECTRON 使用 Hash Router,Web 使用 Browser Router。SSR 客户端则在 debug proxy 时使用 Hash Router,生产使用 Browser Router;如果存在 Sentry release,还会包裹 Browser Router 做错误追踪。
同一份页面组件可以被 Web 和 Electron 消费,但 URL 的生命周期不同:
| 场景 | URL 由谁承载 | 深链策略 |
|---|---|---|
| Web/SSR | Fastify/Vercel/Worker | Browser history + 服务端回退 |
| Electron | 本地窗口 | Hash history + preload 能力 |
| Debug proxy | 开发代理 | Hash history 避免代理回退问题 |
七、错误与安全
SSR 的错误响应带有 traceId,便于日志关联;Meta 注入对动态内容使用 xss,HTML 使用 minify 前先完成结构化注入。服务端不会因为 SEO 失败就把原始异常文本直接写入页面。
网络层的关键取舍是:公开分享页尽量能被匿名读取,但任何需要认证的 mutation 都必须回到客户端认证流程;不要把“SSR 能拿到数据”误解为“客户端拥有同等权限”。