SPA 模式

SPA 模式到底是什么?

对于那些不需要 SSR 来满足 SEO、爬虫或性能要求的应用程序,可以将包含应用程序“Shell”(甚至特定路由的预渲染 HTML)的静态 HTML 文件发送给用户,这些文件包含引导应用程序在客户端运行所需的 htmlheadbody 标签。

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

不使用 SSR 并不意味着放弃服务器端功能! SPA 模式实际上与 Server Functions、Server Routes 或其他外部 API 等服务器端功能搭配得非常好。它仅仅意味着初始文档在客户端使用 JavaScript 渲染之前,不会包含应用程序的完整渲染 HTML

SPA 模式的好处

  • 易于部署 - 只需要一个可以提供静态资源的 CDN。
  • 托管成本更低 - 与 Lambda 函数或长期运行的进程相比,CDN 的成本较低。
  • 纯客户端模式更简单 - 没有 SSR,可以减少水合、渲染和路由方面出错的可能性。

SPA 模式的注意事项

  • 完全内容加载速度较慢 - 由于所有 JS 都必须下载并执行才能渲染 Shell 以下的内容,因此完全内容加载速度会更长。
  • SEO 不太友好 - 除非机器人、爬虫和链接预览器配置为执行 JS 并且应用程序能在合理的时间内渲染完成,否则它们在索引您的应用程序时可能会遇到困难。

它是如何工作的?

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

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

注意

其他路由也可以被预渲染,并且在 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 Shell。任何部署都应按此顺序包含这些优先级:

  1. 确保静态资源始终被提供(如果存在),例如 /about.html。这通常是大多数 CDN 的默认行为。
  2. (可选)允许特定子路径通过到任何动态服务器处理程序,例如 /api/**(更多内容见下文)。
  3. 确保所有 404 请求都被重写到 SPA Shell,例如,进行一个通配符重定向到 /_shell.html(如果您已将 Shell 输出路径配置为自定义值,则使用该自定义值)。

基础重定向示例

我们使用 Netlify 的 _redirects 文件将所有 404 请求重写到 SPA Shell。

# 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

允许使用 Server Functions 和 Server Routes

同样,使用 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 Shell 的默认路径名是 /。我们称之为Shell 掩码路径。由于不会包含匹配的路由,因此用于生成 Shell 的路径名大部分情况下无关紧要,但仍然可以配置。

注意

建议将 Shell 掩码路径的默认值 / 保留。

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

预渲染选项

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

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

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

这意味着,默认情况下,Shell 不会被爬取以查找链接进行进一步预渲染,并且不会重试预渲染失败。

您可以通过提供自己的 prerender 选项来覆盖这些设置。

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 Shell 的 HTML 输出可能很有用,如果您想:

  • 为 SPA 路由提供通用的 head 标签。
  • 提供自定义的挂起回退组件。
  • 更改 Shell 的 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!')
}

您可以使用这个布尔值来根据当前路由是否为 Shell 来有条件地渲染不同的 UI。但请注意,在水合(hydrate)Shell 后,路由器将立即导航到第一个路由,并且 isShell() 将返回 false如果不正确处理,这可能会导致未样式化内容的闪烁。

Shell 中的动态数据

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

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

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

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