TanStack Router 手动 SSR 实战指南:非流式/流式渲染、文档头管理与数据序列化全解析

发布时间:2026/9/16 2:24:18

TanStack Router 手动 SSR 实战指南:非流式/流式渲染、文档头管理与数据序列化全解析 TanStack Router 手动 SSR 实战指南非流式/流式渲染、文档头管理与数据序列化全解析【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router本文围绕 router-core/ssr/SKILL.md 展开系统讲解 TanStack Router当前仓库版本 1.171.15的手动服务端渲染SSR方案包括非流式与流式两种渲染形态、createRequestHandler/defaultRenderHandler/defaultStreamHandler等核心 API、RouterClient/RouterServer双端挂载方式、head路由选项与HeadContent/Scripts/ScriptOnce的文档头管理以及 loader 数据在服务端的脱水dehydrate与客户端注水hydrate原理。读完本文你将掌握在不依赖 TanStack Start 的前提下把 TanStack Router 应用接入已有 Node 服务器如 Express的完整实操路径并理解 SSR 场景下 loader 的 client-first 语义与常见踩坑点。前置认知SSR 的两大铁律在动手之前必须先建立两个关键心智模型它们贯穿整个 SSR 设计SSR API 是实验性的。router-core/ssr目录下的这些 API 与 TanStack Start 共享内部实现未来可能变动。官方明确建议生产环境优先使用 TanStack Start 进行 SSR手动 SSR 只在需要集成既有服务器时使用。TanStack Router 是 client-first 的。默认情况下 loader 只在客户端运行开启 SSR 后 loader 会在客户端与服务端同时运行但绝不是像 Remix / Next.js 那样只跑在服务端。这一语义差异详见>// src/router.tsx import { createRouter as createTanstackRouter } from tanstack/react-router import { routeTree } from ./routeTree.gen export function createRouter() { return createTanstackRouter({ routeTree }) } declare module tanstack/react-router { interface Register { router: ReturnTypetypeof createRouter } }关键在于服务端入口与客户端入口都调用同一个createRouter()保证路由树、配置、插件完全同步避免水合时两侧结构不一致。非流式 SSR 的完整接线非流式 SSR 的接线分为服务端入口与客户端入口两部分。服务端入口使用defaultRenderHandler最简写法是利用内置的默认渲染回调createRequestHandler负责创建 router、构建内存 history、执行 loader 并返回 Web APIResponse// src/entry-server.tsx import { createRequestHandler, defaultRenderHandler, } from tanstack/react-router/ssr/server import { createRouter } from ./router export async function render({ request }: { request: Request }) { const handler createRequestHandler({ request, createRouter }) return await handler(defaultRenderHandler) }从源码看defaultRenderHandler内部就是renderRouterToStringRouterServer /的组合见 defaultRenderHandler.tsxexport const defaultRenderHandler defineHandlerCallback( ({ router, responseHeaders }) renderRouterToString({ router, responseHeaders, children: RouterServer router{router} /, }), )服务端入口使用renderRouterToString自定义包装当需要在 HTML 壳层之外套自定义包装例如注入额外的div idroot或自定义 layout时可以直接用renderRouterToString// src/entry-server.tsx import { createRequestHandler, renderRouterToString, RouterServer, } from tanstack/react-router/ssr/server import { createRouter } from ./router export function render({ request }: { request: Request }) { const handler createRequestHandler({ request, createRouter }) return handler(({ responseHeaders, router }) renderRouterToString({ responseHeaders, router, children: RouterServer router{router} /, }), ) }客户端入口RouterClient水合客户端使用hydrateRoot挂载RouterClient /。注意根元素不能是html标签——RouterClient必须挂载到一个 DOM 元素上通常是在服务端生成的div idroot内部这与服务端可以渲染完整html的能力不同// src/entry-client.tsx import { hydrateRoot } from react-dom/client import { RouterClient } from tanstack/react-router/ssr/client import { createRouter } from ./router const router createRouter() hydrateRoot(document, RouterClient router{router} /)createRequestHandler内部做了什么createRequestHandler是整个 SSR 的服务端核心编排器见 createRequestHandler.ts其关键步骤包括通过getNormalizedURL规范化并净化 pathname解决服务端与浏览器对路径 / search params 编解码不一致的问题源码注释明确指出服务端严格遵循 WHATWG URL 标准而 Chromium/Firefox 在|、대等字符的处理上存在差异见 ssr-server.ts。调用createServerHistory(href)创建服务端内存 history实现在 packages/history/src/index.ts 的createServerHistory再通过router.update({ history, origin })注入 router。router.load({ _signal: request.signal })执行匹配与 loader若结果为redirect直接返回重定向Response。等待router.serverSsr?.dehydrate()完成数据脱水。收集各匹配路由的 headers 合并为responseHeaders默认注入Content-Type: text/html; charsetUTF-8。调用传入的回调即defaultRenderHandler等生成最终响应并把响应体绑定到请求的 AbortSignal 上一旦请求被中断流式响应体会被主动取消并触发serverSsr.cleanup()见 handlerCallback.ts 的bindSsrResponseToRequest。流式 SSR 的完整接线流式渲染的接线模式与非流式完全对称只是把 API 换成defaultStreamHandler/renderRouterToStream。服务端入口使用defaultStreamHandler// src/entry-server.tsx import { createRequestHandler, defaultStreamHandler, } from tanstack/react-router/ssr/server import { createRouter } from ./router export async function render({ request }: { request: Request }) { const handler createRequestHandler({ request, createRouter }) return await handler(defaultStreamHandler) }defaultStreamHandler同样只是renderRouterToStreamRouterServer /的封装见 defaultStreamHandler.tsx并额外把request透传给渲染器用于在流式输出过程中继续感知请求状态。服务端入口使用renderRouterToStream自定义包装// src/entry-server.tsx import { createRequestHandler, renderRouterToStream, RouterServer, } from tanstack/react-router/ssr/server import { createRouter } from ./router export function render({ request }: { request: Request }) { const handler createRequestHandler({ request, createRouter }) return handler(({ request, responseHeaders, router }) renderRouterToStream({ request, responseHeaders, router, children: RouterServer router{router} /, }), ) }流式是自动的文档明确说明Streaming is automatic——只要使用defaultStreamHandler或renderRouterToStreamloader 中未 await 的 deferred promise见 defer.ts 的实现与流式标记都会自动工作无需额外配置。服务端序列化时会对这些延迟数据做增量编码并通过crossSerializeStream推送见 ssr-server.ts。文档头管理head路由选项与HeadContent/Scripts文档头管理Document Head Management是 SSR 应用的核心能力用head路由选项管理title、meta、link、style标签在head中渲染HeadContent /在body末尾渲染Scripts /。根路由配置 head// src/routes/__root.tsx import { createRootRoute, HeadContent, Outlet, Scripts, } from tanstack/react-router export const Route createRootRoute({ head: () ({ meta: [ { charSet: UTF-8 }, { name: viewport, content: widthdevice-width, initial-scale1.0 }, { title: My App }, ], links: [{ rel: icon, href: /favicon.ico }], }), component: RootComponent, }) function RootComponent() { return ( html langen head HeadContent / /head body Outlet / Scripts / /body /html ) }HeadContent组件内部通过useTags收集当前匹配路由的 head 标签并逐个渲染为Asset同时会把router.options.ssr?.nonce透传给每个标签以支持 CSP见 HeadContent.tsx。HeadContent还支持assetCrossOrigin属性用于配置跨域资源加载策略。路由级 head嵌套去重与覆盖子路由的title/meta会覆盖父路由中同name/property的标签这是文档头去重的核心规则。利用这一点可以在 loader 拿到数据后动态生成 SEO 友好的标题与描述// src/routes/posts/$postId.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/posts/$postId)({ loader: async ({ params }) { const post await fetchPost(params.postId) return { post } }, head: ({ loaderData }) ({ meta: [ { title: loaderData.post.title }, { name: description, content: loaderData.post.excerpt }, ], }), component: PostPage, }) function PostPage() { const { post } Route.useLoaderData() return article{post.content}/article }SPA 模式下的 head无完整 HTML 控制权如果应用是纯 SPA没有服务端渲染的 HTML可以把HeadContent /放在组件树顶部渲染。此时无法控制html/body外层结构只能管理头部标签import { createRootRoute, HeadContent, Outlet } from tanstack/react-router const rootRoute createRootRoute({ head: () ({ meta: [{ title: My SPA }], }), component: () ( HeadContent / Outlet / / ), })Body Scripts向body注入脚本head之外的顶层scripts选项用于向body注入脚本注意与head.scripts区分。这些脚本会在应用入口之前执行由Scripts /组件渲染因此Scripts /必须放在body的末尾export const Route createRootRoute({ scripts: () [{ children: console.log(runs before hydration) }], })ScriptOnce预水合脚本的正确姿势ScriptOnce用于渲染一个 SSR 期间立即执行、随后自移除的script客户端导航时它什么都不做避免重复执行。典型场景是主题初始化脚本必须在 React 水合前同步设置html的 class防止首屏闪烁import { ScriptOnce } from tanstack/react-router const themeScript (function() { try { const theme localStorage.getItem(theme) || auto; const resolved theme auto ? (matchMedia((prefers-color-scheme: dark)).matches ? dark : light) : theme; document.documentElement.classList.add(resolved); } catch (e) {} })(); function ThemeProvider({ children }: { children: React.ReactNode }) { return ( ScriptOnce children{themeScript} / {children} / ) }从源码看ScriptOnce.tsxScriptOnce是纯服务端组件在客户端isServer false直接返回null服务端渲染时输出带dangerouslySetInnerHTML的script内容末尾追加;document.currentScript.remove()实现执行后自移除。它同样会透传router.options.ssr?.nonce以兼容 CSP。如果脚本修改了 DOM例如给html添加 class必须在该元素上启用suppressHydrationWarning否则 React 水合会因服务端与客户端 DOM 差异而报警html langen suppressHydrationWarning与 Express 等既有服务器的集成createRequestHandler的输入输出都是Web API的Request/Response。接入 Express 需要做双向格式转换把req转换为Request再把Response写回res。对于流式响应用node:stream/promises的pipeline把响应体管道到res// src/entry-server.tsx import { pipeline } from node:stream/promises import { RouterServer, createRequestHandler, renderRouterToString, } from tanstack/react-router/ssr/server import { createRouter } from ./router import type express from express export async function render({ req, res, }: { req: express.Request res: express.Response }) { const protocol req.get(x-forwarded-proto) ?? req.protocol const host req.get(x-forwarded-host) ?? req.get(host) const url new URL(req.originalUrl || req.url, ${protocol}://${host}).href const request new Request(url, { method: req.method, headers: (() { const headers new Headers() for (const [key, value] of Object.entries(req.headers)) { headers.set(key, value as any) } return headers })(), }) const handler createRequestHandler({ request, createRouter }) const response await handler(({ responseHeaders, router }) renderRouterToString({ responseHeaders, router, children: RouterServer router{router} /, }), ) res.status(response.status) response.headers.forEach((value, name) { res.setHeader(name, value) }) return pipeline(response.body as any, res) }几点实操细节反向代理场景优先读取x-forwarded-proto/x-forwarded-host以还原真实 URL。Origin 安全createRequestHandler内部不会信任可伪造的Origin头来推导 origin源码注释明确引用了 CVE-2024-34351 类似的 SSRF 风险见 ssr-server.ts如果应用部署在代理后面且需要信任转发头应通过 router 的origin选项显式配置受信 origin。数据序列化loader 数据的自动脱水 / 注水SSR 最关键的机制之一是服务端把 loader 结果自动脱水进 HTML客户端再自动注水全程无需手写window.__DATA__之类的样板代码。在 ssr-server.ts 中dehydrateMatch把每个匹配的 route match 压缩为精简结构imatch id、uupdatedAt、sstatus以及可选的b__beforeLoadContext、lloaderData、eerror、ssrSSR 状态字段。脱水后的 router 数据通过crossSerializeStream以window.__TSR__.router...的形式注入脚本见 constants.ts 对应的GLOBAL_TSR定义。序列化使用seroval作为底层引擎内置三个默认插件见 seroval-plugins.tsShallowErrorPlugin支持Error的浅层序列化RawStreamSSRPlugin支持服务端原始流ReadableStreamPlugin支持ReadableStream。因此在 loader 返回值中Date、Error、FormData、undefined等特殊类型开箱即可跨端传输无需额外配置。若需要接入自定义序列化插件例如支持Map、Set等可通过 router 的serializationAdapters选项扩展。服务端还会把脱水数据与注入脚本通过ScriptBuffer缓冲并在渲染结束时统一提升脚本屏障liftBarrier注入到 HTML 中每个脚本末尾追加;document.currentScript.remove()实现自移除避免污染后续 DOM。常见错误与规避Common Mistakes1. HIGHloader 中直接使用浏览器 API由于开启 SSR 后 loader 在客户端和服务端都会执行window/document/localStorage等浏览器 API 在服务端会直接抛错// WRONG — crashes on server loader: async () { const token localStorage.getItem(token) return fetchData(token) } // CORRECT — guard with environment check loader: async () { const token typeof window ! undefined ? localStorage.getItem(token) : null return fetchData(token) }2. MEDIUM基于 hash fragment 做条件渲染hash fragment#section永远不会发送到服务端因此基于window.location.hash的条件渲染必然造成水合不一致。正确做法是把服务端可见的状态放进 search params// WRONG — server has no hash, client does → mismatch component: () { const hash window.location.hash return hash #admin ? AdminPanel / : UserPanel / } // CORRECT — use search params for server-visible state validateSearch: z.object({ view: fallback(z.enum([admin, user]), user) }), component: () { const { view } Route.useSearch() return view admin ? AdminPanel / : UserPanel / }3. CRITICAL套用 Next.js / Remix / react-router-dom 模式TanStack Router 不使用getServerSideProps、getStaticProps、App Router 的page.tsx、Remix 风格的 server-onlyloader导出也不使用react-router-dom的任何 API。错误的目录结构Next.js 风格WRONG (Next.js Pages Router): src/pages/index.tsx src/pages/_app.tsx src/pages/posts/[id].tsx WRONG (Next.js App Router): app/layout.tsx app/page.tsx app/posts/[id]/page.tsx WRONG (Next.js custom App): _app/index.tsx pages/_app.tsx, pages/_document.tsx CORRECT (TanStack Router file-based routing): src/routes/__root.tsx src/routes/index.tsx src/routes/posts/$postId.tsx错误的导入// WRONG — react-router-dom is a different library import { Link, useNavigate, BrowserRouter, Route, Routes, } from react-router-dom // WRONG — Next.js Link/router import Link from next/link import { useRouter } from next/router // Pages Router import { useRouter } from next/navigation // App Router // CORRECT — everything routing-related lives in tanstack/react-router import { Link, useNavigate, useRouter, useLocation, redirect, } from tanstack/react-router错误的数据获取模式// WRONG — Next.js Pages Router export async function getServerSideProps() { return { props: { data: await fetchData() } } } // WRONG — Remix export async function loader({ request }: LoaderFunctionArgs) { return json({ data: await fetchData() }) } // CORRECT — TanStack Router export const Route createFileRoute(/data)({ loader: async () { const data await fetchData() return { data } }, component: DataPage, }) function DataPage() { const { data } Route.useLoaderData() return div{data}/div }判断标准很直接一旦 agent 输出中出现src/pages/、app/layout.tsx、react-router-dom等特征就是为错误框架在写代码——构建会失败或产生运行期冲突的重复/路由。Client-First Loader 与 SSR 的张力这是 TanStack Router SSR 最需要内化的设计哲学浏览器 API 默认可用纯客户端场景但 SSR 下会崩溃需要环境守卫。数据库访问不应放进 loader这与 Remix / Next.js 相反应使用 API 路由承载。若 SSR 场景需要 server-only 的数据逻辑应使用TanStack Start 的 server functions。一句话总结loader 是双端执行的客户端数据加载器不是服务端专属数据入口。loader 的基础语义可进一步阅读 contenteditable="false">【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/16 2:24:18

HISM vs SpawnActor:UE4批量实例化渲染性能优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/16 2:19:17

Vue 2+Element UI后台模板实战指南

简介:这是一套基于Vue 2.x与Element UI开发的饿了么风格后台管理系统完整工程,专为计算机相关专业学生设计,适用于毕业设计、课程设计、实训项目及学科竞赛等实践场景,帮助初学者快速掌握前后端分离开发流程与企业级管理平台构建思…

2026/9/16 2:19:17

QPSK基带仿真全链路解析:从调制映射到载波恢复

简介:一套基于MATLAB的QPSK调制解调仿真代码包,适合通信工程专业学生、科研初学者及相关技术人员快速学习和复现QPSK核心原理。包内包含调制、解调、脉冲成型、星座图绘制、LMS均衡、早迟门与过零定时恢复、四倍频载波恢复等多个m脚本,并附带…

2026/9/16 3:14:19

前端规范体系落地指南:从代码风格到Git提交与接口协作

先聊个很多人没弄明白的事:前端规范不是用来“限制”人的,它是用来“救”人的。我见过太多项目,前期跑得飞快,代码随便写,等到了第6个月、第8个月,新需求来了,改一个老功能要翻半天文件&#xf…

2026/9/16 3:14:19

GitHub四款开源APP实测:小而美精准平替付费软件

最近在 GitHub 上翻开源APP,连着挖到四个让我直呼“够夯”的项目——WhoShitsOnMyC、QRacer、PinToDesk、MouseTrail。它们不是那种上万 Star 的热门框架,而是民间开发者为了解决自己手边的具体问题做出来的小工具、小游戏,但实际用下来&…

2026/9/16 3:14:19

SpringBoot+Vue企业级疫情健康打卡系统架构解析

1. 项目概述:企业级疫情健康打卡系统的技术架构解析这套基于SpringBootVueMyBatisMySQL的企业级疫情打卡系统,是当前企业疫情防控场景下的典型解决方案。系统采用前后端分离架构,后端使用SpringBoot提供RESTful API服务,前端采用V…

2026/9/16 3:14:19

51单片机停车场计费系统设计与Proteus仿真(DS1302+AT24C02)

简介:一套基于51单片机与Protues仿真的停车场刷卡计时计费系统设计资源,面向单片机课程设计、毕业设计及嵌入式入门学习者,完整演示了车辆刷卡进场、出场自动计费结算、时间校准单价设置、车位数量配置及掉电数据保存等核心流程。资源包共47个…

2026/9/16 3:14:19

Windows下Git完整配置:从安装到SSH密钥绑定

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/15 4:54:30

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/16 0:04:09

PHP源码部署实战:从环境配置到运行情侣游戏全攻略

简介:这是一套面向情侣互动场景的PHP完整源码,集成情侣飞行棋、真心话大冒险、情趣骰子等玩法,并内置完整分销制度,可自定义多种返佣比例,源码完全开源无加密,支持微信无感自动授权登录与第三方授权&#x…

2026/9/15 14:22:53

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/15 21:31:11

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/15 11:42:23

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码