选择性服务器端渲染 (SSR)

什么是选择性 SSR?

在 TanStack Start 中,与初始请求匹配的路由默认在服务器上渲染。这意味着 beforeLoadloader 在服务器上执行,然后渲染路由组件。生成的 HTML 被发送到客户端,客户端将标记水合为完全交互式的应用程序。

但是,在某些情况下,您可能希望禁用某些路由或所有路由的 SSR,例如:

  • beforeLoadloader 需要仅限浏览器的 API(例如 localStorage)时。
  • 当路由组件依赖于仅限浏览器的 API(例如 canvas)时。

TanStack Start 的选择性 SSR 功能允许您配置:

  • 哪些路由应在服务器上执行 beforeLoadloader
  • 哪些路由组件应在服务器上渲染。

这与 SPA 模式相比如何?

TanStack Start 的 SPA 模式 完全禁用 beforeLoadloader 的服务器端执行,以及路由组件的服务器端渲染。选择性 SSR 允许您以每路由为基础,静态或动态地配置服务器端处理。

配置

您可以使用 ssr 属性控制路由在初始服务器请求期间的处理方式。如果未设置此属性,则默认为 true。您可以在 createRouter 中使用 defaultSsr 选项更改此默认值。

tsx
// src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
    defaultPendingComponent: () => <div>Loading...</div>,
    // Disable SSR by default
    defaultSsr: false,
  })

  return router
}
// src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
    defaultPendingComponent: () => <div>Loading...</div>,
    // Disable SSR by default
    defaultSsr: false,
  })

  return router
}

ssr: true

这是默认行为,除非另有配置。在初始请求时,它将:

  • 在服务器上运行 beforeLoad 并将结果上下文发送到客户端。
  • 在服务器上运行 loader 并将加载器数据发送到客户端。
  • 在服务器上渲染组件并将 HTML 标记发送到客户端。
tsx
// 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>,
})
// 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

这将禁用服务器端:

  • 路由的 beforeLoadloader 的执行。
  • 路由组件的渲染。
tsx
// 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>,
})
// 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 并将结果上下文发送到客户端。
  • 在服务器上运行 loader 并将加载器数据发送到客户端。
  • 禁用路由组件的服务器端渲染。
tsx
// 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>,
})
// 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。

tsx
// 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>,
})
// 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 函数仅在初始请求期间在服务器上运行,并从客户端捆绑包中剥离。

searchparams 在验证后作为判别联合传入。

tsx
params:
    | { status: 'success'; value: Expand<ResolveAllParamsFromParent<TParentRoute, TParams>> }
    | { status: 'error'; error: unknown }
search:
    | { status: 'success'; value: Expand<ResolveFullSearchSchema<TParentRoute, TSearchValidator>> }
    | { status: 'error'; error: unknown }
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 将为 errorerror 将包含失败详细信息。否则,status 将为 successvalue 将包含经过验证的数据。

继承

在运行时,子路由继承其父路由的选择性 SSR 配置。但是,继承的值只能更改为更严格(即 truedata-onlyfalse,以及 data-onlyfalse)。例如:

tsx
root { ssr: undefined }
  posts { ssr: false }
     $postId { ssr: true }
root { ssr: undefined }
  posts { ssr: false }
     $postId { ssr: true }
  • root 默认为 ssr: true
  • posts 明确设置 ssr: false,因此 beforeLoadloader 都不会在服务器上运行,并且路由组件也不会在服务器上渲染。
  • $postId 设置 ssr: true,但从其父路由继承了 ssr: false。因为继承的值只能更改为更严格,所以 ssr: true 无效,并且继承的 ssr: false 将保持不变。

另一个例子:

tsx
root { ssr: undefined }
  posts { ssr: 'data-only' }
     $postId { ssr: true }
       details { ssr: false }
root { ssr: undefined }
  posts { ssr: 'data-only' }
     $postId { ssr: true }
       details { ssr: false }
  • root 默认为 ssr: true
  • posts 设置 ssr: 'data-only',因此 beforeLoadloader 在服务器上运行,但路由组件不会在服务器上渲染。
  • $postId 设置 ssr: true,但从其父路由继承了 ssr: 'data-only'
  • details 设置 ssr: false,因此 beforeLoadloader 都不会在服务器上运行,并且路由组件也不会在服务器上渲染。这里继承的值被更改为更严格,因此 ssr: false 将覆盖继承的值。

回退渲染

对于第一个设置了 ssr: falsessr: 'data-only' 的路由,服务器将渲染该路由的 pendingComponent 作为回退。如果未配置 pendingComponent,则将渲染 defaultPendingComponent。如果两者都未配置,则不会渲染任何回退。

在客户端进行水合时,即使路由没有定义 beforeLoadloader,此回退也将显示至少 minPendingMs(如果未配置,则为 defaultPendingMinMs)。

如何禁用根路由的 SSR?

您可以禁用根路由组件的服务器端渲染,但 <html> 外壳仍然需要在服务器上渲染。此外壳通过 shellComponent 属性配置,并接受单个属性 childrenshellComponent 始终是 SSR 的,并分别包裹根 component、根 errorComponent 或根 notFound 组件。

一个禁用路由组件 SSR 的根路由的最小设置如下:

tsx
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</>,
  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>
  )
}
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</>,
  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>
  )
}
我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Prisma
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。