跳到主要内容

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 ClientURQL
包大小~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客户端的关键要点:

  1. 选择合适的客户端

    • Apollo Client:功能全面,适合大型项目
    • URQL:轻量高效,适合中小型项目
    • graphql-request:简单场景
  2. 合理使用缓存

    • 理解缓存策略
    • 正确更新缓存
    • 避免缓存不一致
  3. 错误处理

    • 区分GraphQL错误和网络错误
    • 提供友好的用户提示
    • 实现Token刷新机制
  4. TypeScript支持

    • 使用代码生成工具
    • 获得完整的类型安全

好的客户端使用方式可以大大提升开发效率和用户体验。