数据请求方案
概述
在现代前端开发中,数据请求是应用的核心环节。从早期的 XMLHttpRequest 到如今丰富的数据请求库生态,前端开发者拥有了多样化的选择。本文深入分析四种主流数据请求方案:Axios、TanStack Query(React Query)、SWR 和 Apollo GraphQL,从源码原理到实践用法进行全面对比。
Axios 源码分析
Axios 是目前最流行的 HTTP 客户端库,其源码设计蕴含了多个经典的设计模式。
拦截器原理
Axios 的拦截器机制基于链式调用设计。内部维护了两个数组:requestInterceptorChain 和 responseInterceptorChain。当发起请求时,Axios 会将拦截器数组与核心请求方法串联成一个 Promise 链:
// Axios 拦截器核心逻辑简化示意
const chain = [dispatchRequest, undefined] // 核心请求方法占位
let promise = Promise.resolve(config)
// 请求拦截器从后往前插入 chain 前面
this.interceptors.request.forEach(interceptor => {
chain.unshift(interceptor.fulfilled, interceptor.rejected)
})
// 响应拦截器从前往后插入 chain 后面
this.interceptors.response.forEach(interceptor => {
chain.push(interceptor.fulfilled, interceptor.rejected)
})
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}这种设计使得拦截器可以灵活地转换请求配置和响应数据,也支持异步拦截器——只需在拦截器函数中返回 Promise 即可。
适配器模式
Axios 通过适配器模式实现了跨平台能力。核心的 dispatchRequest 方法并不直接发送 HTTP 请求,而是调用配置中的 adapter 函数:
// 默认适配器根据环境自动选择
function getDefaultAdapter() {
let adapter
if (typeof XMLHttpRequest !== 'undefined') {
adapter = adapterXHR // 浏览器端使用 XMLHttpRequest
} else if (typeof process !== 'undefined') {
adapter = adapterHttp // Node.js 端使用 http/https 模块
}
return adapter
}用户也可以传入自定义 adapter,这使得 Axios 可以轻松适配 WeChat、React Native 等不同环境。
请求取消与 CancelToken
Axios 支持两种取消请求的方式:CancelToken(已废弃)和 AbortController(推荐)。
CancelToken 的源码基于观察者模式:
function CancelToken(executor) {
let resolvePromise
this.promise = new Promise(resolve => {
resolvePromise = resolve
})
executor(message => {
if (this.reason) return
this.reason = new Cancel(message)
resolvePromise(this.reason)
})
}当调用取消函数时,this.promise 被 resolve,在适配器内部监听这个 Promise,一旦 resolve 就中止请求。
新版本推荐使用 AbortController:
const controller = new AbortController()
axios.get('/api/data', { signal: controller.signal })
controller.abort() // 取消请求实例化架构
axios.create() 方法返回一个新的 Axios 实例,每个实例拥有独立的拦截器和配置。其实现原理是原型继承:
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig)
const instance = bind(Axios.prototype.request, context)
// 将 Axios 原型上的方法复制到 instance
utils.extend(instance, Axios.prototype, context)
utils.extend(instance, context)
return instance
}这使得每个实例的拦截器、默认配置完全隔离,非常适合微前端或多 API 网关场景。
TanStack Query(React Query)
TanStack Query(前身 React Query)是专为服务端状态管理设计的库,它解决了传统状态管理工具(Redux、Zustand)难以处理的异步数据同步问题。
数据缓存机制
TanStack Query 的核心是一个内存中的查询缓存,以键值对形式存储。每个查询由唯一的 queryKey 标识:
// 缓存条目结构示意
interface QueryCacheEntry {
queryKey: QueryKey
queryHash: string // queryKey 的序列化哈希
state: {
data: TData
status: 'loading' | 'error' | 'success'
dataUpdatedAt: number
error: TError | null
fetchStatus: 'fetching' | 'idle' | 'paused'
}
// 垃圾回收相关
gcTimeout: number | undefined
}缓存采用**垃圾回收(Garbage Collection)**策略。当查询不再被任何组件使用时,启动 GC 定时器(默认 5 分钟),超时后清除缓存。这避免了内存泄漏,同时保持热数据的可用性。
自动重新获取
TanStack Query 提供了多种触发自动重新获取的场景:
- 窗口重新聚焦(refetchOnWindowFocus):当用户切回标签页时自动刷新
- 网络重连(refetchOnReconnect):断网恢复后自动刷新
- 定时刷新(refetchInterval):类似轮询的自动刷新机制
这些策略确保 UI 始终与服务端保持同步,无需手动触发请求。
乐观更新
乐观更新(Optimistic Update)是优化用户体验的重要特性。当用户执行修改操作时,TanStack Query 可以立即更新 UI,同时异步发送请求;如果请求失败则回滚:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 取消可能正在进行的查询
await queryClient.cancelQueries(['todos'])
// 获取当前快照用于回滚
const previousTodos = queryClient.getQueryData(['todos'])
// 立即更新缓存
queryClient.setQueryData(['todos'], old => [...old, newTodo])
// 返回快照上下文
return { previousTodos }
},
onError: (err, newTodo, context) => {
// 请求失败,回滚到原来状态
queryClient.setQueryData(['todos'], context.previousTodos)
},
})无限滚动
useInfiniteQuery 是实现无限滚动/分页加载的专用 Hook。它维护了 pages 数组和 pageParams,每次加载下一页时将新数据追加到 pages 中:
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['users'],
queryFn: ({ pageParam = 1 }) => fetchUsers(pageParam),
getNextPageParam: (lastPage) => {
// 从响应中提取下一页参数
return lastPage.hasNextPage ? lastPage.page + 1 : undefined
},
})DevTools
TanStack Query DevTools 是一个独立的面板,可以实时查看:
- 所有查询的状态(loading/success/error)
- 缓存数据的更新时间戳
- 重新获取频率
- 手动触发刷新/清除缓存
SWR
SWR 由 Vercel 开源,名称源自 HTTP 缓存策略 stale-while-revalidate。
stale-while-revalidate 策略
SWR 的核心策略分为三步:
- 返回缓存(stale):立即从缓存返回旧数据
- 发起请求(revalidate):同时在后台发起网络请求
- 更新 UI:请求回来后用新数据更新,缓存也同步更新
这个策略的直观效果是:用户看到的是即时的页面(即使是旧数据),而非加载状态。SWR 内部通过 useSyncExternalStore(React 18)与 React 并发模式深度集成。
全局配置
SWR 提供灵活的全局配置机制,可以在 SWRConfig 中统一设置所有 SWR Hook 的默认行为:
<SWRConfig value={{
refreshInterval: 30000,
revalidateOnFocus: true,
dedupingInterval: 2000,
errorRetryCount: 3,
onError: (error) => {
console.error('SWR 请求错误:', error)
},
}}>
<App />
</SWRConfig>预加载数据
SWR 支持在渲染前预加载数据,这对于提升页面跳转体验非常有用:
// 在页面跳转前提前发起请求
function prefetchUser(userId: string) {
const cacheKey = `/api/users/${userId}`
mutate(cacheKey, fetch(`/api/users/${userId}`).then(res => res.json()))
}
// 跳转后直接使用缓存数据
function Profile({ userId }: { userId: string }) {
const { data } = useSWR(`/api/users/${userId}`, fetcher)
return <div>{data.name}</div>
}对比 TanStack Query
| 维度 | SWR | TanStack Query |
|---|---|---|
| 包体积 | ~4.5 kB | ~13 kB |
| API 简洁度 | 更简洁,学习成本低 | 功能更丰富,概念更多 |
| 缓存持久化 | 需手动实现 | 内置 persistQueryClient |
| 乐观更新 | 通过 mutate 实现 | 完善的 onMutate/onError/onSettled 生命周期 |
| 无限滚动 | 原生支持 | useInfiniteQuery 更灵活 |
| 框架支持 | React 为主 | React/Vue/Solid/Svelte 全支持 |
SWR 适合轻量、快速上手的项目,而 TanStack Query 在复杂状态管理场景下更强大。
Apollo GraphQL
Apollo GraphQL(Apollo Client)是 GraphQL 生态中最主流的前端客户端库。
缓存策略
Apollo Client 使用归一化缓存(Normalized Cache)。每个 GraphQL 对象根据 __typename 和 id(或自定义 keyFields)被拆分成独立的缓存条目:
// 归一化缓存结构示意
const cacheState = {
'User:1': { __typename: 'User', id: '1', name: 'Alice', posts: ['Post:10', 'Post:11'] },
'Post:10': { __typename: 'Post', id: '10', title: 'Hello' },
'Post:11': { __typename: 'Post', id: '11', title: 'World' },
ROOT_QUERY: { users: ['User:1'] }
}这种结构的优势在于:当某个对象被多个查询引用时,只需更新一次缓存,所有相关 UI 自动同步更新。
缓存策略可通过 fetchPolicy 配置:
cache-first:优先使用缓存(默认)cache-and-network:先返回缓存,同时发起请求更新network-only:始终发起网络请求cache-only:仅使用缓存no-cache:不缓存
查询片段
Fragment 是 GraphQL 的可复用单元。Apollo Client 利用 Fragment 实现缓存一致性:
fragment UserFields on User {
id
name
avatarUrl
}
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
email
}
}当多个查询使用同一个 Fragment 时,Apollo 的归一化缓存能自动合并更新。
订阅
Apollo Client 通过 WebSocket 支持 GraphQL 订阅(Subscription),实现实时数据推送:
const { data, loading } = useSubscription(gql`
subscription OnMessageAdded($chatId: ID!) {
messageAdded(chatId: $chatId) {
id
content
sender { name }
}
}
`, {
variables: { chatId },
})订阅的数据同样会写入归一化缓存,因此如果已有查询显示了相关数据,UI 会自动更新。
分页
Apollo 支持两种分页策略:
基于偏移量(Offset-based):
const { data, fetchMore } = useQuery(GetUsers, {
variables: { offset: 0, limit: 20 },
})
// 加载更多
fetchMore({ variables: { offset: data.users.length } })**基于游标(Cursor-based)**通过 fetchMore 和 updateQuery 实现:
fetchMore({
variables: { cursor: data.users.pageInfo.endCursor },
updateQuery: (prev, { fetchMoreResult }) => {
return {
users: {
...fetchMoreResult.users,
edges: [...prev.users.edges, ...fetchMoreResult.users.edges],
},
}
},
})对比 REST
| 维度 | GraphQL | REST |
|---|---|---|
| 数据获取 | 精确获取所需字段 | 服务端决定返回字段 |
| 批量请求 | 单次请求获取多资源 | 多个端点多次请求 |
| 类型系统 | 强类型 Schema | 无内置类型约束 |
| 缓存 | 归一化缓存,自动合并 | 通常按 URL 缓存 |
| 学习曲线 | 较高(需学 GraphQL + 配置) | 低(标准 HTTP 语义) |
| 工具生态 | Apollo DevTools、GraphQL Playground | Postman、Swagger |
GraphQL 适合数据关系复杂、前端字段需求多变的应用,REST 则更加简单直接。
三种方案对比
| 特性 | Axios | TanStack Query | SWR | Apollo GraphQL |
|---|---|---|---|---|
| 定位 | HTTP 客户端 | 服务端状态管理 | 服务端状态管理 | GraphQL 客户端 |
| 缓存机制 | 无内置缓存 | GC + 内存缓存 | SWR 策略 | 归一化缓存 |
| 自动重新获取 | 需手动实现 | 原生支持 | 原生支持 | 通过 fetchPolicy |
| 乐观更新 | 需手动实现 | 完整生命周期 | mutate 实现 | 需写 update 函数 |
| 请求取消 | CancelToken / AbortController | 通过 signal | 通过 abort | 通过 abort |
| 学习曲线 | 低 | 中 | 低 | 高 |
| 包体积 (min+gzip) | ~14 kB | ~13 kB | ~4.5 kB | ~35 kB |
| TypeScript 支持 | 优秀 | 优秀 | 良好 | 优秀(代码生成) |
| 适用场景 | 任意 HTTP 请求 | 复杂异步状态 | 快速数据同步 | GraphQL 服务 |
TypeScript 类型安全最佳实践
Axios 泛型
interface User {
id: number
name: string
email: string
}
// 通过泛型指定响应数据类型
const { data } = await axios.get<User>('/api/users/1')
// data 的类型被推导为 UserTanStack Query 泛型
// useQuery 有四个泛型参数
// TQueryFnData, TError, TData, TQueryKey
const { data } = useQuery<User, Error>({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})SWR 泛型
// useSWR 支持两个泛型参数
const { data, error } = useSWR<User, Error>(
`/api/users/${userId}`,
fetcher
)Apollo 代码生成
推荐使用 GraphQL Code Generator 从 Schema 自动生成 TypeScript 类型:
# codegen.yml
generates:
src/generated/graphql.ts:
schema: schema.graphql
documents: 'src/**/*.graphql'
plugins:
- typescript
- typescript-operations
- typescript-react-apollo生成的类型与组件无缝集成:
// 自动生成类型安全的 Hook
const { data } = useGetUserQuery({
variables: { id: '1' },
})
// data.user.name 类型安全错误处理与重试策略
Axios 拦截器统一处理
axios.interceptors.response.use(
response => response,
error => {
if (error.response) {
// 服务端返回了错误状态码
switch (error.response.status) {
case 401: redirectToLogin(); break
case 403: showPermissionDenied(); break
case 500: showServerError(); break
}
} else if (error.request) {
// 请求已发出但无响应(网络问题)
showNetworkError()
}
return Promise.reject(error)
}
)TanStack Query 重试机制
TanStack Query 内置指数退避(Exponential Backoff)重试策略:
const query = useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: 3, // 重试次数
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), // 指数退避
retryOnMount: true, // 组件挂载时是否重试
})SWR 错误处理
const { data, error, isValidating, mutate } = useSWR('/api/data', fetcher, {
errorRetryCount: 3,
errorRetryInterval: 5000,
onError: (error, key) => {
// 全局错误处理
if (error.status === 401) {
mutate('/api/auth/token') // 尝试刷新 token
}
},
})统一错误处理模式
推荐将错误边界(Error Boundary)与数据请求结合:
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>出错了:{error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
)
}
// 包裹数据组件
<ErrorBoundary FallbackComponent={ErrorFallback}>
<DataComponent />
</ErrorBoundary>方案选型建议
根据项目需求选择合适的方案:
- 小型项目 / 快速原型:Axios + 手动管理状态(或 SWR)
- 中型 React 项目:SWR(轻量)或 TanStack Query(功能丰富)
- 大型复杂应用:TanStack Query(强缓存管理和乐观更新)
- GraphQL 服务:Apollo Client(首选)或 urql(轻量替代)
- 微前端架构:多个 Axios 实例 + 独立 SWR/TanStack Query 配置
交互演示
以下 Demo 直观对比了三种方案在 GitHub 用户搜索场景下的行为差异: