SPA 模式

什么是 SPA 模式?

对于不需要 SSR 来满足 SEO、爬虫或性能需求的应用程序,可能希望向用户提供包含应用程序"外壳"的静态 HTML(或特定路由的预渲染 HTML),其中包含必要的 htmlheadbody 标签,仅在客户端引导您的应用程序。

为什么在不使用 SSR 的情况下使用 Start?

不使用 SSR 并不意味着放弃服务器端功能! SPA 模式实际上与服务器端功能(如服务器函数和/或服务器路由甚至其他外部 API)配合得很好。它只是意味着初始文档不会包含应用程序的完全渲染 HTML,直到它在客户端使用 JavaScript 渲染

SPA 模式的优势

  • 更容易部署 - 只需要一个可以提供静态资源的 CDN。
  • 托管成本更低 - 与 Lambda 函数或长时间运行的进程相比,CDN 很便宜。
  • 仅客户端更简单 - 没有 SSR 意味着在水合、渲染和路由方面出错的可能性更少。

SPA 模式的注意事项

  • 完整内容时间更慢 - 完整内容时间更长,因为所有 JS 必须下载并执行后才能渲染外壳下方的任何内容。
  • SEO 友好性较差 - 机器人、爬虫和链接展开器_可能_更难索引您的应用程序,除非它们配置为执行 JS 并且您的应用程序可以在合理的时间内渲染。

它是如何工作的?

启用 SPA 模式后,运行 Start 构建将在之后有一个额外的预渲染步骤来生成外壳。这是通过以下方式完成的:

  • 预渲染您应用程序的仅根路由
  • 在您的应用程序通常渲染匹配路由的地方,您的路由器配置的待处理回退组件将被��染
  • 生成的 HTML 存储到名为 /_shell.html 的静态 HTML 页面(可配置)
  • 默认重写配置为将所有 404 请求重定向到 SPA 模式外壳

Note

其他路由也可以被预渲染,建议在 SPA 模式下尽可能多地预渲染,但这不是 SPA 模式工作所必需的。

配置 SPA 模式

要配置 SPA 模式,您可以向 Start 插件的选项中添加一些选项:

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

使用必要的重定向

将纯客户端 SPA 部署到主机或 CDN 通常需要使用重定向来确保 URL 正确重写到 SPA 外壳。任何部署的目标都应该按此顺序包含这些优先级:

  1. 确保静态资源在存在时始终被提供,例如 /about.html。这通常是大多数 CDN 的默认行为
  2. (可选)允许特定子路径路由到任何动态服务器处理程序,例如 /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
# 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
# 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

外壳掩码路径

用于生成 SPA 外壳的默认路径名是 /。我们称之为外壳掩码路径。由于不包括匹配的路由,用于生成外壳的路径名大多无关紧要,但仍然可以配置。

Note

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

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

预渲染选项

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

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

  • outputPath: /_shell.html
  • crawlLinks: false
  • retryCount: 0

这意味着默认情况下,外壳不会被爬取链接以进行额外的预渲染,并且不会重试预渲染失败。

您始终可以通过提供自己的预渲染选项来覆盖这些选项:

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

SPA 模式中的自定义渲染

自定义 SPA 外壳的 HTML 输出在以下情况下很有用:

  • 为 SPA 路由提供通用头部标签
  • 提供自定义的待处理回退组件
  • 更改外壳的 HTML、CSS 和 JS 的任何内容

为了简化这个过程,可以在 router 实例上找到一个 isShell 布尔值:

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

  if (isShell) console.log('Rendering the shell!')
}
// src/routes/root.tsx
export default function Root() {
  const isShell = useRouter().isShell

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

您可以使用此布尔值根据当前路由是否为外壳来有条件地渲染不同的 UI,但请记住,在水合外壳后,路由器将立即导航到第一个路由,isShell 布尔值将为 false如果处理不当,这可能会产生无样式内容的闪烁。

外壳中的动态数据

由于外壳是使用应用程序的 SSR 构建进行预渲染的,因此在根路由上定义的任何 loader 或服务器特定功能都将在预渲染过程中运行,数据将包含在外壳中。

这意味着您可以通过使用 loader 或服务器特定功能在外壳中使用动态数据。

tsx
// 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>
  )
}
// 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>
  )
}