Next.js + TanStack Query 架构中三种常见的数据请求模式
这三种模式各有优劣,适用于不同的场景。
模式一:BFF 代理模式 (Frontend -> Next.js API Route -> Backend)
这种模式的完整流程是:
前端页面组件 -> useXXX
(自定义 Hook) -> xxxService
-> fetch('/api/backend')
-> Next.js API Route -> proxyBackendUtils.request
-> 真正的后端服务
这是一个典型的 BFF (Backend for Frontend) 模式。Next.js 的 API Route 充当了前端和后端之间的代理层和适配层。
优点 (Pros):
- 安全性高: 这是最大的优点。后端的真实地址、API Keys、Secrets、数据库连接字符串等敏感信息永远不会暴露在浏览器端。所有认证和鉴权逻辑都可以在这个安全的服务器环境(Next.js API Route)中处理。
- 解决CORS问题: 浏览器请求的是同源的
/api/backend
,完全没有跨域问题。Next.js 服务器与后端服务之间的通信是服务器到服务器的,不受浏览器同源策略的限制。 - 数据聚合与裁剪: 可以在 API Route 中调用多个后端服务,将数据聚合成一个前端需要的格式,或者剔除掉前端不需要的敏感字段。这大大简化了前端的逻辑。
- 协议转换: 如果后端服务使用的是 gRPC、GraphQL 等不同协议,BFF 层可以将其转换为前端易于消费的 RESTful API。
缺点 (Cons):
- 增加了一次网络跳跃: 请求链路变长了(浏览器 -> Next.js 服务器 -> 后端服务器),理论上会增加一点点延迟。但在内网环境(如 Vercel Serverless Function 与后端服务在同一云服务商的同一区域),这个延迟通常可以忽略不计。
- 增加了维护成本: 你需要编写和维护这层 API Route 代理逻辑。
适用场景:
这是绝大多数生产级应用推荐的模式,尤其是当需要处理认证、隐藏敏感信息、或者前端需要的数据格式与后端直接提供的不一致时。
模式二:前端直连后端模式 (CSR - Client-Side Rendering)
这种模式的完整流程是:
前端页面组件 -> useXXX
-> xxxService
-> fetch('https://api.my-real-backend.com/data')
这是最经典的客户端渲染(CSR)数据请求方式。
优点 (Pros):
- 简单直接: 架构最简单,不需要额外的 BFF 层。
- 延迟较低: 少了一次网络中转,请求直达后端服务。
缺点 (Cons):
- 安全性差: 后端服务的地址直接暴露在前端代码中。如果需要 API Key,也只能使用那些可以安全暴露在前端的 “Public Key”。
- CORS 配置复杂: 后端服务必须正确配置 CORS (Cross-Origin Resource Sharing),允许来自你前端应用的域名请求。
- 前端逻辑复杂: 如果需要聚合多个 API 的数据,这些逻辑都必须在前端完成,增加了前端的复杂度和请求数量。
适用场景:
- 后端是公开的、无需鉴权的 API (例如,天气 API、公共数据查询)。
- 后端服务已经为你这个前端应用做好了完善的 CORS 和安全配置。
- 项目非常简单,不需要 BFF 带来的那些好处。
模式三:Next.js 服务端预取数据 (SSR/SSG)
这种模式的完整流程是:
- 服务端 (Server-Side):
getServerSideProps
或getStaticProps
(或 App Router 中的 Server Component) ->queryClient.prefetchQuery(xXXOptions())
->queryFn
->xxxService
-> 直接请求APP_BACKEND_URL
。 - 数据传递 (Hydration): 服务端将预取的数据序列化后注入到页面 HTML 中。
- 客户端 (Client-Side): 页面在浏览器加载后,
useQuery(xXXOptions())
会立即从已经存在于缓存中的数据(通过 Hydration 获得)中读取,用户不会看到 Loading 状态。
这是一个利用 Next.js 服务端能力结合 TanStack Query 实现高性能初始加载的模式。
优点 (Pros):
- 极致的首屏性能 (LCP): 用户打开页面时,数据已经存在,页面内容立即可见,没有加载中的闪烁。
- 利于 SEO: 搜索引擎爬虫可以直接抓取到包含完整内容的 HTML。
- 高效的数据获取: 在服务端发起请求,通常是服务器到服务器的内网通信,速度远快于客户端通过公网的请求。
- 安全性: 和模式一一样,敏感信息和后端真实地址都保留在服务端。
缺点 (Cons):
- TTFB (Time To First Byte) 变长: 服务器必须等待数据获取完成后才能响应 HTML,如果后端 API 慢,会导致白屏时间变长。
- 增加了服务端负载: 数据请求的压力转移到了 Next.js 服务器上。
- 实现稍复杂: 需要理解
prefetchQuery
、dehydrate
和hydrate
的概念。
适用场景:
- 对 SEO 和首屏性能要求极高的页面,如文章详情页、商品详情页、首页。
- 页面核心内容依赖于异步数据。
总结与最佳实践
这三种模式并不互斥,在一个复杂的应用中,它们往往会组合使用。
特性 | 模式一 (BFF) | 模式二 (直连) | 模式三 (SSR/SSG 预取) |
---|---|---|---|
安全性 | ✅ 高 | ❌ 低 | ✅ 高 |
首屏性能 | 正常 (有 Loading) | 正常 (有 Loading) | ✅ 极佳 |
SEO | 差 | 差 | ✅ 极佳 |
实现复杂度 | 中 | 低 | 高 |
CORS处理 | 无需关心 | 必须处理 | 无需关心 |
典型场景 | 绝大多数客户端后续交互 | 公共API、简单应用 | 核心内容、落地页 |
关键点:xXXOptions()
的重用
你提到的 xXXOptions()
是一个非常关键且优秀的实践。它将 queryKey
和 queryFn
封装在一起。
// a file like: src/queries/xxxQueries.ts// 定义查询选项,这个函数可以在任何地方被导入和使用
export const xxxOptions = (params) => ({queryKey: ['xxx', params],queryFn: () => xxxService.get(params),
});// 在客户端组件中使用
function MyComponent() {const { data } = useQuery(xxxOptions(params));// ...
}// 在服务端预取时使用
export async function getServerSideProps(context) {const queryClient = new QueryClient();await queryClient.prefetchQuery(xxxOptions(params));return {props: {dehydratedState: dehydrate(queryClient),},};
}
为什么这很重要?
因为 TanStack Query 的 Hydration 机制依赖于 完全相同的 queryKey
。通过重用 xxxOptions()
,你可以确保在服务端 prefetchQuery
和在客户端 useQuery
时使用的 queryKey
绝对一致,从而让数据成功地从服务端“水合”到客户端缓存中。
推荐的混合架构 (The “Best of Both Worlds” Approach)
一个强大的现代 Next.js 应用通常会结合 模式一 和 模式三:
-
首次加载 (Initial Load):
- 使用
getServerSideProps
(或 Server Component) 执行 模式三 的逻辑。 - 在
queryFn
中,xxxService
直接通过内网地址 (APP_BACKEND_URL
) 请求后端数据,实现快速、安全的数据预取。 - 页面实现极速加载和 SEO 优化。
- 使用
-
客户端交互 (Client-Side Interactions):
- 当用户在页面上进行操作(如翻页、筛选、点击刷新)时,
useQuery
会触发 refetch,或者useMutation
被调用。 - 此时,
xxxService
中发起的请求应该走 模式一 的 BFF 代理 (/api/backend
)。 - 这样做可以保持客户端交互的安全性,并且
useQuery
的所有强大功能(缓存、后台自动刷新、重试等)都能正常工作。
- 当用户在页面上进行操作(如翻页、筛选、点击刷新)时,
这种组合拳,既利用了 Next.js 的服务端渲染能力来优化首屏,又利用了 BFF 模式来保障后续客户端交互的安全性,同时通过 TanStack Query 统一了数据状态管理,是目前非常先进和稳健的架构选择。