GraphQL 客户端
GraphQL客户端是前端与GraphQL服务器交互的桥梁。一个好的客户端库可以帮助你管理数据获取、缓存、错误处理和实时更新。本章将介绍主流的GraphQL客户端及其使用方法。
为什么需要GraphQL客户端
你可能会问:为什么不直接用 fetch 发送GraphQL查询?
原始方式:
// 使用fetch发送GraphQL查询
async function getUser(id) {
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id }
})
});
const { data, errors } = await response.json();
if (errors) {
throw new Error(errors[0].message);
}
return data.user;
}
这种方式可行,但存在以下问题:
- 没有缓存机制,重复请求浪费资源
- 错误处理需要手动实现
- 加载状态需要自己管理
- 没有类型提示
- 没有实时更新支持
GraphQL客户端库解决了这些问题,提供了开箱即用的解决方案。
Apollo Client
Apollo Client是最流行的GraphQL客户端,功能全面,生态丰富。
安装与配置
npm install @apollo/client graphql
基础配置:
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import { createHttpLink } from '@apollo/client/link/http';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
// HTTP连接
const httpLink = createHttpLink({
uri: 'https://api.example.com/graphql'
});
// 认证中间件
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
};
});
// 错误处理
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(`GraphQL错误: ${message}`);
});
}
if (networkError) {
console.error(`网络错误: ${networkError}`);
// Token过期处理
if (networkError.statusCode === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
}
});
// 创建客户端
const client = new ApolloClient({
link: authLink.concat(errorLink).concat(httpLink),
cache: new InMemoryCache()
});
// 在React应用中使用
function App() {
return (
<ApolloProvider client={client}>
<Router />
</ApolloProvider>
);
}
查询数据
使用useQuery Hook:
import { useQuery, gql } from '@apollo/client';
// 定义查询
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
articles {
id
title
createdAt
}
}
}
`;
function UserProfile({ userId }) {
const { loading, error, data, refetch } = useQuery(GET_USER, {
variables: { id: userId },
// 配置选项
pollInterval: 5000, // 每5秒轮询
fetchPolicy: 'cache-first', // 缓存策略
notifyOnNetworkStatusChange: true
});
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
<h2>文章列表</h2>
{data.user.articles.map(article => (
<ArticleCard key={article.id} article={article} />
))}
<button onClick={() => refetch()}>刷新</button>
</div>
);
}
Fetch Policy(缓存策略):
| 策略 | 说明 |
|---|---|
cache-first | 优先使用缓存,缓存不存在才请求网络(默认) |
cache-only | 只使用缓存,不请求网络 |
network-only | 只请求网络,不使用缓存 |
no-cache | 请求网络但不缓存结果 |
cache-and-network | 先返回缓存数据,同时请求网络更新 |
使用变量和分页:
const GET_ARTICLES = gql`
query GetArticles($filter: ArticleFilterInput, $first: Int, $after: String) {
articles(filter: $filter, first: $first, after: $after) {
edges {
node {
id
title
summary
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
`;
function ArticleList({ status }) {
const { loading, error, data, fetchMore } = useQuery(GET_ARTICLES, {
variables: {
filter: { status },
first: 10
}
});
const loadMore = () => {
fetchMore({
variables: {
after: data.articles.pageInfo.endCursor
}
});
};
if (loading) return <Loading />;
if (error) return <Error error={error} />;
return (
<div>
{data.articles.edges.map(({ node }) => (
<ArticleCard key={node.id} article={node} />
))}
{data.articles.pageInfo.hasNextPage && (
<button onClick={loadMore}>加载更多</button>
)}
</div>
);
}
修改数据
使用useMutation Hook:
import { useMutation, gql } from '@apollo/client';
const CREATE_ARTICLE = gql`
mutation CreateArticle($input: CreateArticleInput!) {
createArticle(input: $input) {
id
title
content
author {
id
name
}
createdAt
}
}
`;
function CreateArticleForm() {
const [createArticle, { loading, error }] = useMutation(CREATE_ARTICLE, {
// 更新缓存
update(cache, { data: { createArticle } }) {
cache.modify({
fields: {
articles(existingArticles = { edges: [] }) {
const newArticleRef = cache.writeFragment({
data: createArticle,
fragment: gql`
fragment NewArticle on Article {
id
title
content
}
`
});
return {
...existingArticles,
edges: [{ node: newArticleRef, __typename: 'ArticleEdge' }, ...existingArticles.edges]
};
}
}
});
},
// 成功回调
onCompleted(data) {
navigate(`/articles/${data.createArticle.id}`);
}
});
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
await createArticle({
variables: {
input: {
title: formData.get('title'),
content: formData.get('content'),
tags: formData.get('tags').split(',').map(t => t.trim())
}
}
});
} catch (err) {
console.error('创建失败:', err);
}
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<input name="tags" placeholder="标签(逗号分隔)" />
<button type="submit" disabled={loading}>
{loading ? '创建中...' : '创建文章'}
</button>
{error && <div className="error">{error.message}</div>}
</form>
);
}
乐观更新:
const UPDATE_ARTICLE = gql`
mutation UpdateArticle($id: ID!, $input: UpdateArticleInput!) {
updateArticle(id: $id, input: $input) {
id
title
content
updatedAt
}
}
`;
function EditArticle({ article }) {
const [updateArticle] = useMutation(UPDATE_ARTICLE, {
optimisticResponse(vars) {
return {
updateArticle: {
__typename: 'Article',
id: vars.id,
title: vars.input.title,
content: vars.input.content,
updatedAt: new Date().toISOString()
}
};
},
rollbackOnError: true // 出错时回滚
});
const handleSave = (newTitle, newContent) => {
updateArticle({
variables: {
id: article.id,
input: { title: newTitle, content: newContent }
}
});
};
// ...
}
缓存管理
Apollo Client的核心特性之一是智能缓存。
缓存结构:
Apollo Client将查询结果规范化存储,相同ID的对象只存储一份。
// 假设查询返回
{
user: {
__typename: 'User',
id: '123',
name: '张三',
articles: [
{
__typename: 'Article',
id: '1',
title: '文章1'
}
]
}
}
// 缓存中存储为
{
'User:123': {
__typename: 'User',
id: '123',
name: '张三',
articles: [{ __ref: 'Article:1' }]
},
'Article:1': {
__typename: 'Article',
id: '1',
title: '文章1'
}
}
手动更新缓存:
import { useApolloClient } from '@apollo/client';
function ArticleActions({ articleId }) {
const client = useApolloClient();
const handleDelete = async () => {
// 删除文章后,从缓存中移除
client.cache.evict({ id: `Article:${articleId}` });
client.cache.gc(); // 垃圾回收
};
// ...
}
自定义缓存ID:
const cache = new InMemoryCache({
typePolicies: {
// 自定义类型的缓存ID生成规则
Article: {
keyFields: ['slug'], // 使用slug作为ID
},
User: {
keyFields: ['id', 'email'], // 复合主键
},
Query: {
fields: {
// 自定义字段合并策略
articles: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
}
}
});
本地状态管理
Apollo Client可以用作全局状态管理工具。
定义本地字段:
const typeDefs = gql`
extend type User {
isLoggedIn: Boolean!
theme: String!
}
`;
const cache = new InMemoryCache({
typePolicies: {
User: {
fields: {
isLoggedIn: {
read() {
return localStorage.getItem('token') !== null;
}
},
theme: {
read(existing) {
return existing || 'light';
}
}
}
}
}
});
// 查询本地状态
const GET_LOCAL_STATE = gql`
query GetLocalState {
isLoggedIn @client
theme @client
}
`;
URQL
URQL是另一个流行的GraphQL客户端,主打轻量和高性能。
安装与配置
npm install urql graphql
import { createClient, Provider } from 'urql';
const client = createClient({
url: 'https://api.example.com/graphql',
fetchOptions: () => {
const token = localStorage.getItem('token');
return {
headers: { authorization: token ? `Bearer ${token}` : '' }
};
},
// 缓存策略
requestPolicy: 'cache-first'
});
function App() {
return (
<Provider value={client}>
<Router />
</Provider>
);
}
查询数据
import { useQuery } from 'urql';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function UserProfile({ userId }) {
const [result, reexecuteQuery] = useQuery({
query: GET_USER,
variables: { id: userId }
});
const { data, fetching, error, stale } = result;
if (fetching) return <Loading />;
if (error) return <Error error={error} />;
return (
<div>
<h1>{data.user.name}</h1>
<button onClick={() => reexecuteQuery({ requestPolicy: 'network-only' })}>
刷新
</button>
</div>
);
}
修改数据
import { useMutation } from 'urql';
const CREATE_ARTICLE = gql`
mutation CreateArticle($input: CreateArticleInput!) {
createArticle(input: $input) {
id
title
}
}
`;
function CreateArticle() {
const [result, createArticle] = useMutation(CREATE_ARTICLE);
const handleSubmit = async (input) => {
const { data, error } = await createArticle({ input });
if (error) {
console.error('创建失败:', error);
return;
}
console.log('创建成功:', data.createArticle);
};
// ...
}
与Apollo对比
| 特性 | Apollo Client | URQL |
|---|---|---|
| 包大小 | ~30KB | ~5KB |
| 缓存 | 智能规范化缓存 | 文档缓存(可扩展) |
| 学习曲线 | 较陡 | 平缓 |
| 生态系统 | 丰富 | 较小 |
| 适用场景 | 大型项目 | 中小型项目 |
GraphQL Request
对于简单的场景,graphql-request是一个轻量级的选择。
npm install graphql-request graphql
import { GraphQLClient, gql } from 'graphql-request';
const client = new GraphQLClient('https://api.example.com/graphql', {
headers: {
authorization: `Bearer ${token}`
}
});
// 查询
const user = await client.request(gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`, { id: '123' });
// 变更
const result = await client.request(gql`
mutation CreateArticle($input: CreateArticleInput!) {
createArticle(input: $input) {
id
title
}
}
`, { input: { title: '标题', content: '内容' } });
订阅(Subscription)
GraphQL订阅允许客户端实时接收服务器推送的数据。
Apollo Client订阅
import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
// HTTP连接用于查询和变更
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql'
});
// WebSocket连接用于订阅
const wsLink = new GraphQLWsLink(createClient({
url: 'wss://api.example.com/graphql',
connectionParams: {
authToken: localStorage.getItem('token')
}
}));
// 根据操作类型分流
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache()
});
使用订阅:
import { useSubscription, gql } from '@apollo/client';
const ON_COMMENT_ADDED = gql`
subscription OnCommentAdded($articleId: ID!) {
onCommentAdded(articleId: $articleId) {
id
content
author {
name
}
createdAt
}
}
`;
function ArticleComments({ articleId }) {
const { data, loading, error } = useSubscription(ON_COMMENT_ADDED, {
variables: { articleId },
// 数据更新时的回调
onData({ client, data }) {
// 可以在这里更新其他查询的缓存
console.log('新评论:', data.data.onCommentAdded);
}
});
// 当有新评论时,data会自动更新
return (
<div>
<h3>实时评论</h3>
{data && <Comment comment={data.onCommentAdded} />}
</div>
);
}
错误处理
Apollo错误处理
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
switch (err.extensions?.code) {
case 'UNAUTHENTICATED':
// Token过期,尝试刷新
return fromPromise(refreshToken()).flatMap(() => {
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: `Bearer ${getNewToken()}`
}
});
return forward(operation);
});
case 'FORBIDDEN':
console.error('权限不足');
break;
case 'BAD_USER_INPUT':
console.error('输入验证失败:', err.message);
break;
default:
console.error('GraphQL错误:', err.message);
}
}
}
if (networkError) {
if (networkError.statusCode === 401) {
// 重定向到登录页
window.location.href = '/login';
} else {
console.error('网络错误:', networkError);
}
}
});
组件级错误处理
import { useQuery } from '@apollo/client';
function UserProfile({ userId }) {
const { loading, error, data } = useQuery(GET_USER, {
variables: { id: userId },
errorPolicy: 'all' // 即使有错误也返回部分数据
});
if (loading) return <Loading />;
// 错误可能是数组
if (error) {
return (
<div className="error-container">
{error.graphQLErrors.map((err, i) => (
<div key={i} className="error-item">
{err.message}
</div>
))}
{error.networkError && (
<div className="error-item">
网络错误: {error.networkError.message}
</div>
)}
</div>
);
}
return <UserDisplay user={data.user} />;
}
最佳实践
查询组织
使用片段复用字段选择:
const USER_FRAGMENT = gql`
fragment UserFields on User {
id
name
email
avatar
createdAt
}
`;
const ARTICLE_FRAGMENT = gql`
fragment ArticleFields on Article {
id
title
summary
createdAt
author {
...UserFields
}
}
${USER_FRAGMENT}
`;
// 在多个查询中复用
const GET_ARTICLE = gql`
query GetArticle($id: ID!) {
article(id: $id) {
...ArticleFields
content
}
}
${ARTICLE_FRAGMENT}
`;
const GET_USER_ARTICLES = gql`
query GetUserArticles($userId: ID!) {
user(id: $userId) {
...UserFields
articles {
...ArticleFields
}
}
}
${USER_FRAGMENT}
${ARTICLE_FRAGMENT}
`;
配合TypeScript
生成类型定义:
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
# codegen.yml
schema: https://api.example.com/graphql
documents: src/**/*.graphql
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
使用生成的类型:
import { useQuery, GetUsersQuery } from './generated/graphql';
function UserList() {
const { data, loading } = useQuery(GetUsersDocument);
// data 有完整的类型提示
const users: GetUsersQuery['users'] = data?.users ?? [];
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
性能优化
延迟加载查询:
import { useLazyQuery } from '@apollo/client';
function SearchPage() {
const [search, { loading, data }] = useLazyQuery(SEARCH_QUERY);
const handleSearch = (keyword) => {
search({ variables: { keyword } });
};
return (
<div>
<SearchInput onSearch={handleSearch} />
{loading && <Loading />}
{data && <SearchResults results={data.search} />}
</div>
);
}
预加载:
function ArticleLink({ article }) {
const client = useApolloClient();
// 鼠标悬停时预加载
const handleMouseEnter = () => {
client.query({
query: GET_ARTICLE_DETAIL,
variables: { id: article.id }
});
};
return (
<Link
to={`/articles/${article.id}`}
onMouseEnter={handleMouseEnter}
>
{article.title}
</Link>
);
}
总结
GraphQL客户端的关键要点:
-
选择合适的客户端:
- Apollo Client:功能全面,适合大型项目
- URQL:轻量高效,适合中小型项目
- graphql-request:简单场景
-
合理使用缓存:
- 理解缓存策略
- 正确更新缓存
- 避免缓存不一致
-
错误处理:
- 区分GraphQL错误和网络错误
- 提供友好的用户提示
- 实现Token刷新机制
-
TypeScript支持:
- 使用代码生成工具
- 获得完整的类型安全
好的客户端使用方式可以大大提升开发效率和用户体验。