TanStack Router SSR/SSG 最佳实践指南 (Cloudflare Pages 版)

15 阅读5分钟

TanStack Router SSR/SSG 最佳实践指南 (Cloudflare Pages 版)

本文档基于生产环境验证的架构,提炼了使用 TanStack Router 进行 SSR/SSG 混合部署的通用最佳实践。本方案特别针对 Cloudflare Pages 平台进行了优化,旨在实现高性能、高 SEO 友好度且低维护成本的现代 Web 应用。


1. 核心架构设计:静态优先,动态兜底 (Hybrid Architecture)

1.1 设计理念

  • 默认静态 (SSG):对所有公开、内容相对固定的页面(营销页、博客、文档)进行构建期预渲染。这保证了最佳的 TTFB(首字节时间)和 CDN 缓存能力。
  • SSR 兜底 (SSR Fallback):仅对无法预渲染的长尾路径或需要实时数据的 SEO 页面启用运行时服务端渲染。
  • 客户端渲染 (CSR):对需要登录状态、高度交互的应用内页面(Dashboard、设置),直接降级为 SPA 模式,无需 SSR。

1.2 URL 分层策略

路由类型渲染模式适用场景示例
强静态SSG (预渲染)首页、关于我们、法律条款/, /about
内容流SSG + ISR*博客文章、产品详情/blog/$slug
应用态CSR (SPA)用户后台、购物车/dashboard/*
动态 SEOSSR搜索结果、即时生成的落地页/search, /promo/$id

*注:Cloudflare Pages 暂不支持增量静态再生 (ISR),通常通过“预渲染 + 定时重建”或“预渲染首屏 + 客户端数据更新”替代。


2. 路由系统改造 (Router Refactoring)

2.1 工厂模式 (Factory Pattern)

在 SSR 环境下,必须避免单例模式。每个请求都需要独立的 Router 实例,以防止跨请求的状态污染。

// ❌ 错误:单例模式
export const router = createRouter({ ... })

// ✅ 正确:工厂模式
export function createRouter() {
  return createRouter({
    routeTree,
    context: { ... }, // 注入请求级上下文
  })
}

2.2 入口分离

标准的 SSR 架构需要拆分客户端和服务端入口:

  • entry-client.tsx:负责客户端“注水” (Hydration)。

    const router = createRouter()
    router.hydrate() // 恢复服务端状态
    ReactDOM.hydrateRoot(document.getElementById('root')!, <RouterProvider router={router} />)
    
  • entry-server.tsx:负责服务端渲染与脱水 (Dehydration)。

    export async function render(url, headAssets) {
      const router = createRouter()
      const memoryHistory = createMemoryHistory({ initialEntries: [url] })
      router.update({ history: memoryHistory })
      
      await router.load() // 等待关键数据加载
      
      // 注入 Head 资源并渲染
      // 返回 { appHtml, dehydratedRouter }
    }
    

3. Cloudflare Functions 集成 (The Critical Part)

3.1 构建期生成入口 (Build-time Generation)

不要手动维护 functions/[[path]].ts。应在构建脚本中动态生成它,以便将 index.html 中带有哈希的文件名(如 assets/index-Ah3...css)自动注入到 SSR 模板中。

流程

  1. 构建客户端 (vite build) → 产出 dist/client (含带哈希的静态资源)。
  2. 构建服务端 (vite build --ssr) → 产出 dist/server
  3. 脚本生成 Functions 入口:读取 dist/client/index.html 提取 CSS/JS 标签,写入 functions/[[path]].ts

3.2 静态资源绕行 (Static Bypass) - 核心稳定性机制

SSR 最常见的问题是错误地拦截了静态资源请求(如 .css, .js, robots.txt),导致返回 HTML 内容,引发 MIME 类型错误或 React Hydration 错误 (Error 418)。

最佳实践逻辑

// functions/[[path]].ts
export const onRequest = async (context) => {
  const { pathname } = new URL(context.request.url);
  const accept = context.request.headers.get('accept') || '';
  
  // 1. 扩展名检查:任何带扩展名的请求视为静态资源
  const hasExt = /\/[^/]+\.[^/]+$/.test(pathname);
  
  // 2. Accept 头检查:不接受 HTML 的请求视为非页面请求
  const acceptsHtml = /\btext\/html\b/i.test(accept);
  
  // 3. 绕行判断:非 GET、有扩展名、或不接受 HTML -> 直接透传给静态层
  if (context.request.method !== 'GET' || hasExt || !acceptsHtml) {
    return context.next();
  }
  
  // ... 进入 SSR 逻辑
}

3.3 SSR 路由白名单 (Gating)

为了防止 404 页面或未知的 SPA 路由意外触发 SSR(导致不必要的计算开销或错误),建议引入SSR 路径白名单

  • 机制:利用 prerender-routes.jsonsitemap 作为白名单。
  • 逻辑:如果请求路径不在白名单中,跳过 SSR,直接返回 SPA 的 index.html(由 Cloudflare 默认行为处理)。

4. SEO 与 Head 管理

4.1 集中式 Head 策略

利用 TanStack Router 的 Context 功能,在根路由 (__root.tsx) 统一管理全局 SEO 标签。

  • Hreflang & Canonical:基于当前 URL 动态生成,避免在每个页面重复手动配置。
  • Title & Description:通过 loader 获取数据,通过 head 函数返回。

4.2 避免 Hydration Mismatch

服务端生成的 Title/Meta 必须与客户端初始渲染完全一致。

  • 技巧:不要在组件内部使用 useEffect 修改 Title。使用 Router 的 head 属性,它能确保服务端渲染时就输出正确的 <title> 标签。

5. 构建与部署管道 (Pipeline)

5.1 推荐构建顺序

  1. build:client:生成静态产物。
  2. build:server:生成 SSR 渲染器。
  3. generate:functions:生成 Cloudflare Functions 入口(注入 CSS/JS)。
  4. prerender:本地运行 SSR 渲染器,生成高优先级路由的静态 HTML 文件。
  5. post-build
    • 生成 sitemap.xmlrobots.txt
    • 生成 _redirects 规则文件。
    • 清理 HTML 中的开发时标签(如 /src/*)。

5.2 重定向规则 (_redirects)

对于 Cloudflare Pages,正确的重定向规则是性能和稳定性的关键。

# 1. 静态资源优先 (防止 SSR 误拦截)
/assets/*  /assets/:splat  200
/images/*  /images/:splat  200

# 2. 规范化规则 (移除尾随斜杠等)
# 注意:不要强制添加尾随斜杠,这会破坏文件请求

# 3. SPA 兜底 (对于未被 SSR/SSG 覆盖的路径)
/*  /index.html  200

6. 常见故障排查 (Troubleshooting)

现象可能原因解决方案
CSS/JS 报 MIME 错误SSR 拦截了静态资源并返回了 HTML检查 onRequest 中的静态资源绕行逻辑;检查 _redirects 是否有强制重写规则。
React Error #418服务端 HTML 与客户端渲染不一致检查 entry-server 是否注入了正确的脱水数据;检查是否有组件使用了 window 变量但未做环境判断。
页面空白 / 307 跳转路由初始化 URL 不正确确保服务端传入 createMemoryHistory 的 URL 是规范化的(包含 pathname + search)。
样式闪烁 (FOUC)CSS 未在 HTML 头部注入确保 generate:functions 脚本正确提取了 index.html 中的 <link rel="stylesheet"> 并注入到 SSR 模板中。

7. 总结

本方案的核心优势在于确定性

  1. 构建确定性:通过构建脚本注入哈希资源,杜绝版本不一致。
  2. 路由确定性:通过白名单和静态绕行,确保 SSR 只在应该发生的时候发生。
  3. SEO 确定性:通过集中式 Head 管理,保证元数据的准确输出。