TL;DR
GraphQL 客户端负责前端的查询执行、缓存和状态管理。Apollo Client 功能最丰富,提供规范化缓存和开发工具。urql 提供更轻量的基于插件的架构。Relay 凭借编译器驱动的方式在大规模应用中表现出色。根据项目规模、团队经验和性能要求进行选择。
关键要点
- Apollo Client 提供规范化缓存、乐观更新 UI,拥有所有 GraphQL 客户端中最丰富的生态系统。
- useQuery 和 useMutation 钩子封装了加载、错误和重新获取逻辑,使组件代码更简洁。
- 缓存规范化按 ID 存储实体,当数据在任何位置发生变化时自动更新 UI。
- 基于游标的分页配合 fetchMore 和 Relay 风格连接比偏移量分页的扩展性更好。
- graphql-codegen 从 schema 生成 TypeScript 类型,实现端到端的类型安全。
- WebSocket 订阅支持实时通知和聊天等实时功能。
- urql 凭借其基于 exchange 的插件系统,是较简单项目的有力替代方案。
- 速率限制、深度限制和查询复杂度分析保护你的 GraphQL API 免受滥用。
1. GraphQL 基础
GraphQL 是一种 API 查询语言,让客户端能精确请求所需的数据。它支持三种操作类型:查询用于读取数据,变更用于写入数据,订阅用于实时更新。与 REST 不同,单个 GraphQL 端点处理所有操作,客户端指定响应的形状。
# 查询 — 获取用户及其嵌套的文章
query GetUser($id: ID!) {
user(id: $id) {
name
email
posts(first: 5) {
title
createdAt
}
}
}
# 变更 — 创建新文章
mutation CreatePost($input: PostInput!) {
createPost(input: $input) {
id
title
}
}2. Apollo Client 设置
Apollo Client 是最流行的 React GraphQL 客户端。它结合了智能的规范化缓存、可组合的链式请求处理和内置错误处理。链式链接允许你以模块化方式添加认证头、记录错误和重试失败请求。
import {
ApolloClient, InMemoryCache,
createHttpLink, from
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
const httpLink = createHttpLink({
uri: '/graphql',
});
const authLink = setContext((_, { headers }) => ({
headers: {
...headers,
authorization: localStorage.getItem('token')
? 'Bearer ' + localStorage.getItem('token')
: '',
},
}));
const errorLink = onError(({ graphQLErrors }) => {
if (graphQLErrors) graphQLErrors.forEach(e =>
console.error('[GQL]', e.message));
});
export const client = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache(),
});3. useQuery 与 useMutation
Apollo 提供 React 钩子实现声明式数据获取。useQuery 自动处理加载状态、错误、轮询和重新获取。useMutation 返回触发函数并跟踪变更生命周期。两个钩子都与缓存集成,使 UI 响应式更新。
import { useQuery, useMutation, gql } from '@apollo/client';
const GET_TODOS = gql`
query { todos { id text done } }
`;
const TOGGLE = gql`
mutation Toggle($id: ID!) {
toggleTodo(id: $id) { id done }
}
`;
function TodoList() {
const { data, loading, error } = useQuery(GET_TODOS);
const [toggle] = useMutation(TOGGLE, {
optimisticResponse: ({ id }) => ({
toggleTodo: { __typename: 'Todo', id, done: true },
}),
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return data.todos.map(t => (
<li key={t.id} onClick={() => toggle({ variables: { id: t.id } })}>
{t.text} {t.done ? '(done)' : ''}
</li>
));
}4. 缓存管理
Apollo InMemoryCache 使用规范化缓存,将查询结果拆分为按类型名和 ID 存储的独立对象。这意味着在一个查询中更新用户对象会自动在所有地方更新它。类型策略允许你自定义字段的读取、合并和键的生成方式。
const cache = new InMemoryCache({
typePolicies: {
User: {
keyFields: ['email'], // 使用 email 作为缓存键
},
Query: {
fields: {
posts: {
keyArgs: ['category'],
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
read(existing) {
return existing;
},
},
},
},
},
});
// 变更后手动更新缓存
cache.modify({
id: cache.identify({ __typename: 'User', email: 'a@b.com' }),
fields: {
postCount(prev) { return prev + 1; },
},
});5. 分页
基于游标的分页是 GraphQL 推荐的方式。它使用不透明游标标记列表中的位置,避免了偏移量分页在插入或删除项时的问题。Relay 连接规范提供了包含 edges、nodes 和 pageInfo 的标准模式。
const GET_FEED = gql`
query Feed($cursor: String) {
feed(first: 10, after: $cursor) {
edges { node { id title } cursor }
pageInfo { hasNextPage endCursor }
}
}
`;
function Feed() {
const { data, fetchMore } = useQuery(GET_FEED);
const loadMore = () => fetchMore({
variables: {
cursor: data.feed.pageInfo.endCursor,
},
});
return (
<div>
{data?.feed.edges.map(e => (
<p key={e.node.id}>{e.node.title}</p>
))}
{data?.feed.pageInfo.hasNextPage && (
<button onClick={loadMore}>加载更多</button>
)}
</div>
);
}6. 代码生成
graphql-codegen 读取你的 schema 和操作来生成 TypeScript 类型、类型化钩子和文档节点。这消除了手动类型定义,并在构建时捕获类型错误。typed-document-node 插件生成零运行时开销的类型,适用于任何 GraphQL 客户端。
# codegen.yml
schema: 'http://localhost:4000/graphql'
documents: 'src/**/*.graphql'
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typed-document-node
# 使用生成的类型
# src/queries/user.graphql
# query GetUser($id: ID!) {
# user(id: $id) { id name email }
# }
# 在组件中使用 — 完全类型化,无需手动定义类型
# import { GetUserDocument } from './generated/graphql';
# const { data } = useQuery(GetUserDocument, {
# variables: { id: '1' }, // 类型检查
# });
# data?.user.name; // 自动补全有效7. 订阅
GraphQL 订阅使用 WebSocket 连接从服务器推送实时更新。Apollo Client 通过 graphql-ws 支持订阅,维护持久连接。你可以使用 useSubscription 进行独立监听,或使用 subscribeToMore 将实时数据附加到现有查询。
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
const wsLink = new GraphQLWsLink(
createClient({ url: 'ws://localhost:4000/graphql' })
);
const httpLink = new HttpLink({ uri: '/graphql' });
const splitLink = split(
({ query }) => {
const def = getMainDefinition(query);
return def.kind === 'OperationDefinition'
&& def.operation === 'subscription';
},
wsLink, httpLink,
);
// useSubscription 钩子
// const { data } = useSubscription(ON_MESSAGE);8. 错误处理
GraphQL 可以同时返回部分数据和错误,不像 REST 通常只返回单个状态码。Apollo 的 errorPolicy 控制此行为:"none" 遇到错误时抛出异常,"all" 同时返回数据和错误,"ignore" 静默丢弃错误。将错误链接与 React 错误边界结合使用可实现健壮的错误处理。
import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
const retryLink = new RetryLink({
delay: { initial: 300, max: 3000, jitter: true },
attempts: { max: 3, retryIf: (error) => !!error },
});
const errorLink = onError(
({ graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
if (err.extensions?.code === 'UNAUTHENTICATED') {
window.location.href = '/login';
}
}
}
if (networkError) {
console.error('网络错误:', operation.operationName);
}
}
);
// 链接链:retryLink -> errorLink -> httpLink9. 测试 GraphQL 组件
Apollo 提供 MockedProvider 来包裹组件,在测试期间使用预定的查询响应。你可以模拟加载状态、错误和成功响应,而无需连接真实服务器。对于钩子级别的测试,testing-library 的 renderHook 可以与 Apollo 模拟配合使用。
import { MockedProvider } from '@apollo/client/testing';
import { render, screen, waitFor } from '@testing-library/react';
const mocks = [{
request: {
query: GET_TODOS,
variables: {},
},
result: {
data: {
todos: [
{ id: '1', text: 'Write tests', done: false },
{ id: '2', text: 'Ship code', done: true },
],
},
},
}];
test('renders todos', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<TodoList />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByText('Write tests')).toBeTruthy();
});
});10. 性能优化
Apollo 支持查询批处理,将多个操作合并为单个 HTTP 请求以减少网络开销。持久化查询发送哈希而非完整查询字符串,缩小请求大小并支持 CDN 缓存。使用 @defer 的延迟查询让你逐步渲染解析较慢的字段。
import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
// 将多个查询批处理为一个请求
const batchLink = new BatchHttpLink({
uri: '/graphql',
batchMax: 5,
batchInterval: 20,
});
// 持久化查询 — 发送哈希而非完整查询
const persistedLink = createPersistedQueryLink({
sha256, useGETForHashedQueries: true,
});
// @defer 用于渐进式加载
// query GetPost($id: ID!) {
// post(id: $id) {
// title
// body
// ... @defer { comments { id text } }
// }
// }11. urql 替代方案
urql 是一个轻量级 GraphQL 客户端,基于 exchange 的插件系统。它的包体积大约是 Apollo 的一半,默认使用更简单的文档缓存。自定义 exchange 允许你添加规范化缓存、认证、重试逻辑等。当你需要更薄的抽象层时,urql 是一个强有力的选择。
import { Client, cacheExchange, fetchExchange, Provider }
from 'urql';
import { authExchange } from '@urql/exchange-auth';
const client = new Client({
url: '/graphql',
exchanges: [
cacheExchange,
authExchange(async (utils) => ({
addAuthToOperation(operation) {
const token = localStorage.getItem('token');
return token ? utils.appendHeaders(operation, {
Authorization: 'Bearer ' + token,
}) : operation;
},
didAuthError(error) {
return error.graphQLErrors.some(
e => e.extensions?.code === 'UNAUTHORIZED'
);
},
async refreshAuth() {
localStorage.removeItem('token');
window.location.href = '/login';
},
})),
fetchExchange,
],
});12. Relay Modern
Relay 是 Facebook 的 GraphQL 客户端,专为大规模应用设计。其编译器在构建时静态分析查询,生成优化的运行时产物。Relay 强制数据遮蔽,意味着组件只能访问通过 fragment 明确请求的数据,确保没有隐式的数据依赖。
// UserProfile.tsx — Relay fragment 组件
import { graphql, useFragment } from 'react-relay';
const UserProfileFragment = graphql`
fragment UserProfile_user on User {
name
avatar
bio
followerCount
}
`;
function UserProfile({ userRef }) {
const data = useFragment(UserProfileFragment, userRef);
return (
<div>
<img src={data.avatar} alt={data.name} />
<h2>{data.name}</h2>
<p>{data.bio}</p>
<span>{data.followerCount} 关注者</span>
</div>
);
}13. GraphQL 安全
GraphQL 灵活的查询语言意味着客户端可以构造深度嵌套或昂贵的查询。保护你的 API 需要深度限制(拒绝超过 N 层的查询)、查询复杂度分析(为字段分配成本)和速率限制(按客户端或 IP 限流)。在生产环境中禁用内省以防止 schema 泄露。
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule }
from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7),
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 5,
listFactor: 10,
}),
],
introspection: process.env.NODE_ENV !== 'production',
});
// 使用 express 中间件进行速率限制
// app.use('/graphql', rateLimit({
// windowMs: 60 * 1000,
// max: 100,
// }));试试我们相关的开发者工具
FAQ
新项目应该用 Apollo Client 还是 urql?
如果你需要规范化缓存、乐观 UI、订阅支持和大型生态系统,选择 Apollo Client。如果你偏好更小的包体积、更简单的 API,并且可以接受文档级缓存,选择 urql。对于有严格数据访问模式的大规模应用,考虑 Relay。
GraphQL 缓存与 REST 缓存有什么不同?
REST 使用 HTTP 缓存头和按端点的 CDN 缓存。GraphQL 客户端实现应用级缓存。Apollo 按类型名和 ID 规范化数据,因此更新一个实体会更新所有引用它的查询。这比 HTTP 缓存更强大,但需要仔细配置缓存策略。
graphql-codegen 值得花时间设置吗?
值得。代码生成从你的 schema 提供完整的 TypeScript 类型,在构建时捕获类型错误,生成类型化钩子,并消除手动类型维护。初始设置大约需要 15 分钟,随着项目增长可节省大量调试时间。
如何在 Apollo Client 中处理认证?
使用 setContext 链接从存储中读取认证令牌,并将其作为 Authorization 头附加到每个请求上。对于令牌刷新,使用错误链接捕获 401 错误,刷新令牌,然后使用 forward() 重试失败的操作。
什么时候该用订阅,什么时候用轮询?
对于真正的实时数据(如聊天消息或实时仪表盘),且低延迟很重要时使用订阅。对于每隔几秒更新一次且可以接受轻微延迟的数据,使用轮询(useQuery 配合 pollInterval)。轮询实现和扩展都更简单。
如何防止服务端的 N+1 查询问题?
使用 DataLoader 在单个 GraphQL 操作中批量处理和去重数据库请求。DataLoader 收集单个 tick 中请求的所有 ID,然后执行一次批量查询。每个请求初始化一个新的 DataLoader 以避免跨用户缓存。
可以不用专用客户端库来使用 GraphQL 吗?
可以。你可以使用 fetch 或 graphql-request 将查询作为普通 HTTP POST 请求发送。但是你会失去自动缓存、乐观更新和订阅支持。对于超出简单数据获取的任何应用,建议使用专用客户端。
如何在生产环境中保护 GraphQL API?
实现深度限制、查询复杂度分析和速率限制。在生产环境中禁用内省。使用持久化查询来白名单允许的操作。添加认证和字段级授权。监控查询性能并为解析器设置超时限制。