SPA 模式

SPA 模式到底是什么?

对于不需要 SSR 来实现 SEO、爬虫或性能的应用程序,可能需要向用户提供静态 HTML,其中包含应用程序的“外壳”(甚至特定路由的预渲染 HTML),其中包含必要的 htmlheadbody 标签,以便仅在客户端引导应用程序。

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

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

SPA 模式的优点

  • 更易于部署 - 您只需要一个可以提供静态资产的 CDN。
  • 更便宜的托管费用 - 与 Lambda 函数或长时间运行的进程相比,CDN 便宜得多。
  • 仅限客户端更简单 - 没有 SSR 意味着水合、渲染和路由出错的可能性更小。

SPA 模式的注意事项

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

它是如何工作的?

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

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

注意

其他路由也可以预渲染,建议在 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

Shell 掩码路径

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

注意

建议将 / 的默认值保留为外壳掩码路径。

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 路由提供通用 head 标签
  • 提供自定义的挂起回退组件
  • 更改外壳的 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如果处理不当,这可能会导致未样式化内容的闪烁。

Shell 中的动态数据

由于外壳是使用应用程序的 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>
  )
}
我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Prisma
订阅 Bytes

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

Bytes

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

订阅 Bytes

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

Bytes

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