跳到主要内容

网络 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.getlists.get 等协议方法,因为 SSR 的职责是根据 URL 渲染资源,不需要把所有数据持久化到本地 SQLite。

二、Web/SSR 的双层应用

apps/ssr 同时包含:

  • Fastify 服务端入口:index.tssrc/router/global.ts
  • React 客户端:client/App.tsxclient/router.tsxclient/pages
  • Vite/tsdown/Worker/Vercel 构建配置;
  • Meta、OG、hydration 和错误处理。

这个组合让分享页具有服务端首屏和 SEO 能力,但它也要求团队明确“服务器代码”和“浏览器代码”的 import 边界。

三、请求到分享页的完整链路

四、Meta 注入为什么要单独一层

src/router/global.tssafeInjectMetaToTemplate 负责把异常分类:

  • 资源不存在 → 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/SSRFastify/Vercel/WorkerBrowser history + 服务端回退
Electron本地窗口Hash history + preload 能力
Debug proxy开发代理Hash history 避免代理回退问题

七、错误与安全

SSR 的错误响应带有 traceId,便于日志关联;Meta 注入对动态内容使用 xss,HTML 使用 minify 前先完成结构化注入。服务端不会因为 SEO 失败就把原始异常文本直接写入页面。

网络层的关键取舍是:公开分享页尽量能被匿名读取,但任何需要认证的 mutation 都必须回到客户端认证流程;不要把“SSR 能拿到数据”误解为“客户端拥有同等权限”。