TanStack Start 中文文档
渲染

SPA 模式

SPA 模式到底是什么?

对于不需要 SSR 来满足 SEO、爬虫或性能要求的应用,你可能希望给用户提供只包含应用「外壳(shell)」的静态 HTML(甚至可以为特定路由提供预渲染 HTML)。这些 HTML 带有必要的 htmlheadbody 标签,仅在客户端启动(bootstrap)你的应用。

为什么要用没有 SSR 的 Start?

没有 SSR 不意味着放弃服务端特性! SPA 模式其实与服务器函数(Server Functions)、服务器路由(Server Routes)甚至其他外部 API 等服务端特性配合得很好。它仅仅意味着初始文档不会包含应用完全渲染后的 HTML,直到应用在客户端用 JavaScript 渲染出来

SPA 模式的优点

  • 更容易部署——只需要一个能托管静态资源的 CDN。
  • 托管更便宜——与 Lambda 函数或常驻进程相比,CDN 便宜得多。
  • 仅客户端更简单——没有 SSR,意味着水合、渲染和路由方面出问题的可能性更少。

SPA 模式的注意事项

  • 到达完整内容更慢——因为必须下载并执行所有 JS,外壳(shell)以下的内容才能渲染,所以到达完整内容的时间更长。
  • 对 SEO 不太友好——爬虫和链接解包器(link unfurlers)_可能_更难索引你的应用,除非它们被配置为执行 JS,并且你的应用能在合理时间内渲染完成。

它是如何工作的?

启用 SPA 模式后,运行 Start 构建会额外执行一个预渲染步骤来生成外壳。具体流程是:

  • 只预渲染你应用的根路由
  • 在应用通常会渲染匹配路由的地方,改为渲染你的路由器配置的pending 兜底组件(pending fallback component)
  • 生成的 HTML 被保存为名为 /_shell.html 的静态 HTML 页面(可配置)
  • 默认配置了重写规则,把所有 404 请求重定向到 SPA 模式的外壳

注意

其他路由也可以预渲染,并且建议在 SPA 模式下尽可能多地预渲染,但这并不是 SPA 模式能工作的必要条件。

配置 SPA 模式

要配置 SPA 模式,可以在 Start 插件的选项中添加几个选项:

vite.config.ts
export default defineConfig({
  plugins: [
    tanstackStart({
      spa: {
        enabled: true,
      },
    }),
  ],
})

译者注:SPA 模式适合什么场景?

如果你的应用是纯管理后台、内部工具这类不需要 SEO 的交互型应用,SPA 模式可以大幅简化部署(纯静态托管)。但它牺牲了首屏速度和 SEO,所以对外内容型站点不建议使用。Start 的取舍在于:即便用 SPA 模式,你依然能享受服务器函数、服务器路由等服务端能力,只是首屏不渲染完整 HTML 而已。

使用必要的重定向

把纯客户端 SPA 部署到主机或 CDN 时,通常需要使用重定向,确保 URL 被正确重写到 SPA 外壳。任何部署的目标都应包含以下优先级(按顺序):

  1. 确保静态资源如果存在就始终被提供,比如 /about.html。这通常是大多数 CDN 的默认行为
  2. (可选)把特定子路径加入白名单,路由到动态服务器 handler,比如 /api/**(下面有更多说明)
  3. 确保所有 404 请求被重写到 SPA 外壳,比如用兜底重定向到 /_shell.html(如果你把外壳输出路径配置成了自定义值,就用那个)

基础重定向示例

我们用 Netlify 的 _redirects 文件把所有 404 请求重写到 SPA 外壳。

# Catch all other 404 requests and rewrite them to the SPA shell
/* /_shell.html 200

允许服务器函数和服务器路由

同样用 Netlify 的 _redirects 文件,我们可以把特定子路径加入白名单,让它们路由到服务器。

# Allow requests to /_serverFn/* to be routed through to the server (If you have configured your server function base path to be something other than /_serverFn, use that instead)
/_serverFn/* /_serverFn/:splat 200

# Allow any requests to /api/* to be routed through to the server (Server routes can be created at any path, so you must ensure that any server routes you want to use are under this path, or simply add additional redirects for each server route base you want to expose)
/api/* /api/:splat 200

# Catch all other 404 requests and rewrite them to the SPA shell
/* /_shell.html 200

外壳掩码路径(Shell Mask Path)

用于生成 SPA 外壳的默认路径名是 /。我们称之为外壳掩码路径。由于匹配到的路由不会被包含,用于生成外壳的路径名基本无关紧要,但它仍然是可以配置的。

注意

建议保持默认值 / 作为外壳掩码路径。

vite.config.ts
export default defineConfig({
  plugins: [
    tanstackStart({
      spa: {
        maskPath: '/app',
      },
    }),
  ],
})

预渲染选项

prerender 选项用于配置 SPA 外壳的预渲染行为,它接受与预渲染指南中相同的预渲染选项。

默认情况下,设置了以下 prerender 选项:

  • outputPath/_shell.html
  • crawlLinksfalse
  • retryCount0

这意味着默认情况下,外壳不会为了额外的预渲染而爬取链接,也不会重试预渲染失败。

你可以通过提供自己的预渲染选项来覆盖这些默认值:

vite.config.ts
export default defineConfig({
  plugins: [
    tanstackStart({
      spa: {
        prerender: {
          outputPath: '/custom-shell',
          crawlLinks: true,
          retryCount: 3,
        },
      },
    }),
  ],
})

SPA 模式的自定义渲染

自定义 SPA 外壳的 HTML 输出会很有用,如果你想:

  • 为 SPA 路由提供通用的 head 标签
  • 提供自定义的 pending 兜底组件
  • 修改外壳 HTML、CSS 和 JS 的几乎任何东西

为了简化这个过程,router 实例上有一个 isShell() 函数:

// src/routes/root.tsx
export default function Root() {
  const isShell = useRouter().isShell()

  if (isShell) console.log('Rendering the shell!')
}

你可以用这个布尔值,根据当前路由是否是外壳来有条件地渲染不同的 UI。但要注意,水合外壳之后,路由器会立即导航到第一个路由,此时 isShell() 会返回 false如果处理不当,这可能导致无样式内容闪烁(flashes of unstyled content,FOUC)。

外壳中的动态数据

由于外壳是使用应用的 SSR 构建预渲染的,你在根路由上定义的任何 loader 或服务器专属功能都会在预渲染过程中运行,数据会被包含在外壳中。

这意味着你可以在外壳中使用动态数据,只要用 loader 或服务器专属功能即可。

// src/routes/__root.tsx

export const RootRoute = createRootRoute({
  loader: async () => {
    return {
      name: 'Tanner',
    }
  },
  component: Root,
})

export default function Root() {
  const { name } = useLoaderData()

  return (
    <html>
      <body>
        <h1>Hello, {name}!</h1>
        <Outlet />
      </body>
    </html>
  )
}

On this page