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

发布时间:2026/11/14 3:38:09

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/11/13 17:12:31

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/11/10 2:04:31

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

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

2026/11/13 13:46:30

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

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

2026/11/14 3:37:11

安监局高级电工证多久拿证?过期了怎么复审?一文讲透

安监局高级电工证多久拿证?过期了怎么复审?一文讲透 证书过期了不知道怎么复审延期?你不是一个人。很多电工朋友在拿到【安监局高级电工证】后,可能因为工作变动、换岗或疏忽,导致证书过期。但别急,这篇文章不仅告诉你怎么复审,还会告诉你从报名到拿证…

2026/11/14 3:37:11

通化安监局电工证查询官方回应:怕被坑?这几点必须知道

通化安监局电工证查询官方回应:怕被坑?这几点必须知道 你是不是也这样,想考个电工证,结果一问三不知,不知道去哪报名,怕被中介骗?官方回应来了,别再乱花钱了。今天我从一个老电工的角度,带你从头到尾梳理通化安监局电工证查询全流程,教你避坑,少走…

2026/11/14 3:37:11

青海电工证快速拿证怎么网上查询最靠谱

青海电工证快速拿证怎么网上查询最靠谱 你还在为找不到电工证而发愁吗?工地要开工、项目要推进, 急需特种作业证才能上岗找工作 ,但办理流程复杂、耗时长,还容易被坑。别急,这篇文章就带你从 青海电工证快速拿证 的全过程讲起, 网上查询…

2026/11/14 3:37:11

菏泽电工证怎么查询 真实经历告诉你工地太忙怎么应对

菏泽电工证怎么查询 真实经历告诉你工地太忙怎么应对 工地太忙根本没时间复习考试,这几乎是每个一线电工的真实写照。尤其是在河南公路工程这样的项目现场,时间紧、任务重,很多人连抽空复习的机会都没有,更别说通过考试拿到电工证了。但如果你正打算考证…

2026/11/14 3:32:11

郑州电工证怎么申请网上查询流程全解析

郑州电工证怎么申请网上查询流程全解析 学历不高怕报不上名?其实郑州电工证申请并不难,关键是你得知道怎么走流程。最近有位许昌的转岗工人王师傅就问过我,他学历只有初中,担心自己连报名资格都没有。结果他按照网上查询的流程一步步来,不仅顺利拿到了低…

2026/11/13 22:33:32

电工证丢失还能补吗?千万别踩坑,丢了证等于失业

电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。…

2026/11/14 0:06:39

土木特种工考证官方报名入口怎么找 之前考的证在外省能不能转过来

土木特种工考证官方报名入口怎么找 之前考的证在外省能不能转过来 之前考的证在外省能不能转过来?这个问题我太熟悉了,作为一名干了十几年的特种作业证书办理从业者,每年都会遇到不少像你一样在外地考了证,回来却发现无法直接使用的学员。别急,今天我就…

2026/11/14 0:06:39

电工证要多久多钱保姆级教程:工地太忙根本没时间复习考试

电工证要多久多钱保姆级教程:工地太忙根本没时间复习考试 工地太忙根本没时间复习考试,你不是一个人。在信阳浉河区,很多刚进入工地的电工都面临这样的困境:工作强度大,时间碎片化,根本没有系统复习考试的机会。但电工证是上岗的硬门槛,…

2026/11/13 22:32:11

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/13 12:14:39

SSM电子书店源码全解析:结构、原理、部署与排错实战

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

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

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

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