SSR(Server-Side Rendering)是 TanStack Router 提供的服务端渲染能力。需要先注意:资料明确说明 SSR API 处于实验阶段,与 TanStack Start 共享内部实现,可能发生变化;生产环境推荐使用 TanStack Start 进行 SSR,仅在需要与现有服务器集成时才使用手动 SSR 方案。另一个关键点是 TanStack Router 是客户端优先(CLIENT-FIRST)的:loader 默认在客户端运行,开启 SSR 后会同时在客户端和服务端运行,并非 Remix/Next.js 那样仅在服务端运行。
一、SSR 的两种形式
- 非流式(Non-streaming):整页在服务端渲染完成后,作为单个 HTML 响应发送,然后在客户端注水(hydrate)。
- 流式(Streaming):优先立即发送关键首屏内容,其余内容在解析完成时增量流式传输。
关键行为包括:服务端自动使用内存历史(memory history,因为没有 window);loader 数据在服务端自动脱水(dehydrate)、在客户端自动注水;数据序列化开箱即支持 Date、Error、FormData 和 undefined。
二、共享 Router 工厂
router 必须在服务端和客户端以相同方式创建。需要从共享文件导出一个工厂函数(createRouter),并在 @tanstack/react-router 的 Register 接口中注册 router 类型。
三、非流式 SSR
服务端入口有两种写法:使用 createRequestHandler 配合 defaultRenderHandler;或在需要自定义 wrapper 时使用 renderRouterToString 与 RouterServer。客户端入口使用 react-dom/client 的 hydrateRoot 配合来自 @tanstack/react-router/ssr/client 的 RouterClient。
四、流式 SSR
服务端入口同样有两种写法:使用 createRequestHandler 配合 defaultStreamHandler;或使用 renderRouterToStream 与 RouterServer。资料说明流式是自动的:延迟数据(loader 中未 await 的 promise)与流式标记在使用 defaultStreamHandler 或 renderRouterToStream 时即可正常工作。
五、文档头部(Document Head)管理
通过路由的 head 选项管理 title、meta、link、style 标签。在 head 中渲染 HeadContent,在 body 中渲染 Scripts。根路由的 head 可返回包含 charSet、viewport、title 的 meta 以及 links。每个路由的 head 支持嵌套去重:子路由的 title 与同名/同 property 的 meta 会覆盖父路由。对于没有服务端渲染 HTML 的 SPA,在组件树顶部渲染 HeadContent。
六、Body 脚本与预注水脚本
使用 scripts(独立于 head.scripts)在应用入口前向 body 注入脚本,由 Scripts 组件渲染,应放在 body 末尾。ScriptOnce 在 SSR 期间渲染一个立即执行并自我移除的 script,客户端导航时不做任何事(不会重复执行)。若脚本修改了 DOM(例如给 html 添加 class),需要在元素上使用 suppressHydrationWarning。
七、Express 集成示例
createRequestHandler 接收 Web API Request 并返回 Web API Response。文档给出了在 Express 中转换格式的完整示例:从请求中构造 URL 与 Headers,创建 Request,调用 handler 并使用 renderRouterToString 与 RouterServer,将状态码与响应头写回 Express 响应,最后通过 pipeline 将响应体流式传输到 res。
八、常见错误
- 高优先级:在 loader 中不使用环境检查就调用浏览器 API。由于 SSR 下 loader 在两端运行,window、document、localStorage 会在服务端抛错,需要
typeof window !== 'undefined'之类的判断。 - 中优先级:对服务端渲染内容使用 hash 片段。hash(#section)不会发送到服务端,基于 hash 的条件渲染会造成注水不匹配,应改用 search params 维护服务端可见状态。
- 关键:生成 Next.js、Remix 或 react-router-dom 的模式。TanStack Router 不使用 getServerSideProps、getStaticProps、App Router 的 page.tsx、Remix 风格的服务端专用 loader 导出,也不使用 react-router-dom。文档列出了错误的文件结构(src/pages/、app/layout.tsx、pages/_app.tsx、pages/_document.tsx)与错误的导入(react-router-dom、next/link、next/router、next/navigation),并给出正确结构(src/routes/__root.tsx、src/routes/index.tsx、src/routes/posts/$postId.tsx)与正确写法(路由相关全部来自 @tanstack/react-router,loader 写在 createFileRoute 的 loader 中并通过 Route.useLoaderData() 读取)。若在输出中看到 src/pages/、app/layout.tsx、react-router-dom 等,说明生成了错误框架的代码,构建会失败或在运行时产生冲突的 / 路由。
九、客户端优先 loader 与 SSR 的张力
TanStack Router 的 loader 设计上就是客户端优先,开启 SSR 后会在两端运行,这意味着:浏览器 API 默认可用(仅客户端),但在 SSR 下会出问题;数据库访问不应放在 loader 中(与 Remix/Next 不同),应使用 API 路由;若要在 SSR 下进行仅服务端的取数逻辑,应使用 TanStack Start 的 server functions。loader 基础可参见 router-core/data-loading。
十、交叉引用
router-core/data-loading:SSR 会改变 loader 的执行位置。compositions/router-query:与 TanStack Query 一起进行 SSR 脱水/注水。