选择性服务端渲染(Selective SSR)
什么是选择性 SSR?
在 TanStack Start 中,匹配初始请求的路由默认会在服务器上渲染。这意味着 beforeLoad 和 loader 会在服务器上执行,然后渲染路由组件。生成的 HTML 被发送给客户端,客户端把标记水合成一个完全可交互的应用。
然而,有些情况下你可能想对某些路由或所有路由禁用 SSR,比如:
- 当
beforeLoad或loader需要浏览器专属 API(例如localStorage)。 - 当路由组件依赖浏览器专属 API(例如
canvas)。
TanStack Start 的选择性 SSR(Selective SSR)特性让你配置:
- 哪些路由应该在服务器上执行
beforeLoad或loader。 - 哪些路由组件应该在服务器上渲染。
与 SPA 模式相比如何?
TanStack Start 的 SPA 模式完全禁用 beforeLoad 和 loader 的服务端执行,也禁用路由组件的服务端渲染。选择性 SSR 允许你按路由配置服务端处理,可以是静态的,也可以是动态的。
配置
你可以用 ssr 属性控制路由在初始服务端请求中的处理方式。如果未设置该属性,默认值为 true。你可以用 createStart 中的 defaultSsr 选项改变这个默认值:
// src/start.ts
import { createStart } from '@tanstack/react-start'
export const startInstance = createStart(() => ({
// Disable SSR by default
defaultSsr: false,
}))ssr: true
这是默认行为(除非另行配置)。在初始请求时,它会:
- 在服务器上运行
beforeLoad,并把生成的 context 发送给客户端。 - 在服务器上运行
loader,并把加载器数据发送给客户端。 - 在服务器上渲染组件,并把 HTML 标记发送给客户端。
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
ssr: true,
beforeLoad: () => {
console.log('Executes on the server during the initial request')
console.log('Executes on the client for subsequent navigation')
},
loader: () => {
console.log('Executes on the server during the initial request')
console.log('Executes on the client for subsequent navigation')
},
component: () => <div>This component is rendered on the server</div>,
})ssr: false
这会禁用服务端的:
- 路由
beforeLoad和loader的执行。 - 路由组件的渲染。
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
ssr: false,
beforeLoad: () => {
console.log('Executes on the client during hydration')
},
loader: () => {
console.log('Executes on the client during hydration')
},
component: () => <div>This component is rendered on the client</div>,
})ssr: 'data-only'
这个混合选项会:
- 在服务器上运行
beforeLoad,并把生成的 context 发送给客户端。 - 在服务器上运行
loader,并把加载器数据发送给客户端。 - 禁用路由组件的服务端渲染。
// src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
ssr: 'data-only',
beforeLoad: () => {
console.log('Executes on the server during the initial request')
console.log('Executes on the client for subsequent navigation')
},
loader: () => {
console.log('Executes on the server during the initial request')
console.log('Executes on the client for subsequent navigation')
},
component: () => <div>This component is rendered on the client</div>,
})函数形式
为了获得更多灵活性,你可以使用 ssr 属性的函数形式,在运行时决定是否对路由做 SSR:
// src/routes/docs/$docType/$docId.tsx
export const Route = createFileRoute('/docs/$docType/$docId')({
validateSearch: z.object({ details: z.boolean().optional() }),
ssr: ({ params, search }) => {
if (params.status === 'success' && params.value.docType === 'sheet') {
return false
}
if (search.status === 'success' && search.value.details) {
return 'data-only'
}
},
beforeLoad: () => {
console.log('Executes on the server depending on the result of ssr()')
},
loader: () => {
console.log('Executes on the server depending on the result of ssr()')
},
component: () => <div>This component is rendered on the client</div>,
})ssr 函数只在初始请求期间运行于服务器上,并且会从客户端打包产物中剥离。
search 和 params 在校验后以判别联合(discriminated union)的形式传入:
params:
| { status: 'success'; value: Expand<ResolveAllParamsFromParent<TParentRoute, TParams>> }
| { status: 'error'; error: unknown }
search:
| { status: 'success'; value: Expand<ResolveFullSearchSchema<TParentRoute, TSearchValidator>> }
| { status: 'error'; error: unknown }如果校验失败,status 会是 error,error 包含失败详情。否则,status 会是 success,value 包含校验后的数据。
继承
在运行时,子路由会继承父级的选择性 SSR 配置。但继承的值只能变得更严格(即 true 变成 data-only 或 false,data-only 变成 false)。例如:
root { ssr: undefined }
posts { ssr: false }
$postId { ssr: true }root默认为ssr: true。posts显式设置ssr: false,所以beforeLoad和loader都不会在服务器上运行,路由组件也不会在服务器上渲染。$postId设置了ssr: true,但从父级继承了ssr: false。因为继承的值只能变得更严格,ssr: true不起作用,继承的ssr: false会保留。
另一个例子:
root { ssr: undefined }
posts { ssr: 'data-only' }
$postId { ssr: true }
details { ssr: false }root默认为ssr: true。posts设置ssr: 'data-only',所以beforeLoad和loader在服务器上运行,但路由组件不在服务器上渲染。$postId设置了ssr: true,但从父级继承了ssr: 'data-only'。details设置ssr: false,所以beforeLoad和loader都不会在服务器上运行,路由组件也不会在服务器上渲染。这里继承的值被变得更严格,因此ssr: false会覆盖继承的值。
译者注:继承只会更严格
选择性 SSR 的继承规则是「只能往更禁用方向走」:父级关掉了 SSR(false),子级就不能再打开(true 无效);父级是 data-only,子级可以降到 false,但不能升回 true。这是有意设计,保证嵌套路由树的行为可预测、不会出现「父级不渲染但子级渲染」的矛盾。
兜底渲染
对于第一个 ssr: false 或 ssr: 'data-only' 的路由,服务器会把路由的 pendingComponent 作为兜底渲染。如果没有配置 pendingComponent,会渲染 defaultPendingComponent。如果两者都没配置,就不会渲染兜底。
在客户端水合期间,这个兜底至少会显示 minPendingMs(如果未配置则用 defaultPendingMinMs),即使路由没有定义 beforeLoad 或 loader。
如何禁用根路由的 SSR?
你可以禁用根路由组件的服务端渲染,但 <html> 外壳仍然需要在服务器上渲染。这个外壳通过 shellComponent 属性配置,它接收一个 children 属性。shellComponent 总是被 SSR,它分别包裹着根 component、根 errorComponent 或根 notFound 组件。
一个为路由组件禁用 SSR 的根路由最小配置是这样的:
import * as React from 'react'
import {
HeadContent,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router'
export const Route = createRootRoute({
shellComponent: RootShell,
component: RootComponent,
errorComponent: () => <div>Error</div>,
notFoundComponent: () => <div>Not found</div>,
ssr: false, // or `defaultSsr: false` on the router
})
function RootShell({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}
function RootComponent() {
return (
<div>
<h1>This component will be rendered on the client</h1>
<Outlet />
</div>
)
}