Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

SPA 模式

究竟什么是 SPA 模式?

对于那些不需要为了 SEO、爬虫或性能原因而使用 SSR 的应用程序,将静态 HTML 发送给用户可能更为理想。这个静态 HTML 包含了应用程序的“壳”(Shell)(甚至是特定路由的预渲染 HTML),其中包含必要的 html、head 和 body 标签,仅在客户端启动应用程序。

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

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

SPA 模式的优势

SPA 模式的局限性

它是如何工作的?

启用 SPA 模式后,运行 Start 构建将增加一个额外的预渲染步骤来生成“壳”。其步骤如下:

配置 SPA 模式

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

// 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 壳:

# 捕获所有其他 404 请求并将其重写到 SPA 壳
/* /_shell.html 200

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

同样,使用 Netlify 的 _redirects 文件,我们可以将特定子路径列入白名单,以便路由到服务器:

# 允许请求 /_serverFn/* 路由到服务器
/_serverFn/* /_serverFn/:splat 200

# 允许任何对 /api/* 的请求路由到服务器
/api/* /api/:splat 200

# 捕获所有其他 404 请求并将其重写到 SPA 壳
/* /_shell.html 200

壳掩码路径 (Shell Mask Path)

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

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

预渲染选项

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

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

这意味着默认情况下,系统不会抓取壳中的链接以进行额外的预渲染,也不会重试失败的预渲染。你可以随时覆盖这些选项:

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

在 SPA 模式下自定义渲染 (Customized rendering in SPA mode)

在以下场景中,自定义 SPA 壳的 HTML 输出会非常有用:

为了简化这一过程,你可以通过 router 实例获取 isShell() 函数:

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

  if (isShell) console.log("正在渲染壳 (Shell)!");

  // 你可以根据 isShell 的布尔值进行条件渲染
}

你可以利用这个布尔值根据当前是否为“壳”来渲染不同的 UI。但请注意:在壳完成注水(Hydrate)后,路由器会立即导航到首个路由,此时 isShell() 将返回 false。如果处理不当,这可能会导致样式未加载或内容闪烁(Flashes of unstyled content)。


在壳中使用动态数据 (Dynamic Data in your Shell)

由于壳是使用应用程序的 SSR 构建版本进行预渲染的,因此在根路由 (Root Route) 上定义的任何 loader 或服务器特定功能都会在预渲染过程中运行,并且这些数据会被包含在生成的壳中。

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

// src/routes/__root.tsx

export const RootRoute = createRootRoute({
  // 这个 loader 将在预渲染壳时在服务器端运行
  loader: async () => {
    return {
      name: "Tanner",
    };
  },
  component: Root,
});

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

  return (
    <html>
      <body>
        <h1>你好, {name}!</h1>
        {/* 在 SPA 模式下渲染壳时,
            Outlet 通常会渲染你配置的 pendingFallbackComponent 
        */}
        <Outlet />
      </body>
    </html>
  );
}