GraphQL 客户端开发指南
GraphQL 客户端开发是构建现代前端应用的关键部分。以下是使用 Apollo Client 和其他 GraphQL 客户端的全面指南。
1. Apollo Client 配置
基本配置
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql',
credentials: 'include'
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all'
},
query: {
fetchPolicy: 'network-only',
errorPolicy: 'all'
},
mutate: {
errorPolicy: 'all'
}
}
});
带认证的配置
import { ApolloClient, InMemoryCache, createHttpLink, ApolloLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
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 client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
2. 查询数据
使用 useQuery Hook
import { useQuery, gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers($limit: Int, $offset: Int) {
users(limit: $limit, offset: $offset) {
id
name
email
createdAt
}
}
`;
function UserList() {
const { loading, error, data, fetchMore } = useQuery(GET_USERS, {
variables: { limit: 10, offset: 0 },
notifyOnNetworkStatusChange: true
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
<button
onClick={() => fetchMore({
variables: { offset: data.users.length },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
users: [...prev.users, ...fetchMoreResult.users]
};
}
})}
>
Load More
</button>
</div>
);
}
使用 lazy query
import { useLazyQuery, gql } from '@apollo/client';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function UserSearch() {
const [getUser, { loading, error, data }] = useLazyQuery(GET_USER);
const [userId, setUserId] = useState('');
return (
<div>
<input
value={userId}
onChange={(e) => setUserId(e.target.value)}
placeholder="Enter user ID"
/>
<button onClick={() => getUser({ variables: { id: userId } })}>
Search
</button>
{loading && <div>Loading...</div>}
{error && <div>Error: {error.message}</div>}
{data && (
<div>
<h3>{data.user.name}</h3>
<p>{data.user.email}</p>
</div>
)}
</div>
);
}
3. 修改数据
使用 useMutation Hook
import { useMutation, gql } from '@apollo/client';
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
function CreateUserForm() {
const [createUser, { loading, error }] = useMutation(CREATE_USER, {
update(cache, { data: { createUser } }) {
cache.modify({
fields: {
users(existingUsers = []) {
const newUserRef = cache.writeFragment({
data: createUser,
fragment: gql`
fragment NewUser on User {
id
name
email
}
`
});
return [...existingUsers, newUserRef];
}
}
});
}
});
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
createUser({
variables: {
input: { name, email }
}
});
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <div>Error: {error.message}</div>}
</form>
);
}
乐观更新
const [updateUser, { loading }] = useMutation(UPDATE_USER, {
optimisticResponse: (variables) => ({
updateUser: {
__typename: 'User',
id: variables.id,
name: variables.input.name,
email: variables.input.email
}
}),
update(cache, { data: { updateUser } }) {
cache.writeFragment({
id: `User:${updateUser.id}`,
fragment: gql`
fragment UpdateUser on User {
name
email
}
`,
data: updateUser
});
}
});
4. 缓存管理
配置缓存策略
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
keyArgs: ['filter', 'sort'],
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
},
read(existing, { args: { offset, limit } }) {
return existing && existing.slice(offset, offset + limit);
}
},
user: {
read(_, { args, toReference }) {
return toReference({
__typename: 'User',
id: args.id
});
}
}
}
},
User: {
keyFields: ['id', 'email']
}
}
});
手动更新缓存
import { useApolloClient } from '@apollo/client';
function UpdateUserButton({ userId, newData }) {
const client = useApolloClient();
const handleClick = () => {
client.writeFragment({
id: `User:${userId}`,
fragment: gql`
fragment UserFragment on User {
name
email
}
`,
data: newData
});
};
return <button onClick={handleClick}>Update User</button>;
}
清除缓存
function ClearCacheButton() {
const client = useApolloClient();
const handleClick = () => {
client.clearStore();
};
return <button onClick={handleClick}>Clear Cache</button>;
}
5. 分页
基于偏移的分页
const GET_POSTS = gql`
query GetPosts($offset: Int, $limit: Int) {
posts(offset: $offset, limit: $limit) {
id
title
content
author {
name
}
}
}
`;
function PostList() {
const { loading, data, fetchMore } = useQuery(GET_POSTS, {
variables: { offset: 0, limit: 10 }
});
return (
<div>
{data?.posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
<button
onClick={() => fetchMore({
variables: { offset: data.posts.length },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
posts: [...prev.posts, ...fetchMoreResult.posts]
};
}
})}
>
Load More
</button>
</div>
);
}
基于游标的分页
const GET_POSTS = gql`
query GetPosts($after: String, $first: Int) {
posts(after: $after, first: $first) {
edges {
node {
id
title
content
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function PostList() {
const { loading, data, fetchMore } = useQuery(GET_POSTS, {
variables: { first: 10 }
});
return (
<div>
{data?.posts.edges.map(({ node }) => (
<div key={node.id}>
<h3>{node.title}</h3>
<p>{node.content}</p>
</div>
))}
{data?.posts.pageInfo.hasNextPage && (
<button
onClick={() => fetchMore({
variables: { after: data.posts.pageInfo.endCursor },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
posts: {
...fetchMoreResult.posts,
edges: [
...prev.posts.edges,
...fetchMoreResult.posts.edges
]
}
};
}
})}
>
Load More
</button>
)}
</div>
);
}
6. 错误处理
处理 GraphQL 错误
function UserList() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <div>Loading...</div>;
if (error) {
if (error.graphQLErrors) {
return (
<div>
GraphQL Errors:
{error.graphQLErrors.map((err, i) => (
<div key={i}>{err.message}</div>
))}
</div>
);
}
if (error.networkError) {
return <div>Network Error: {error.networkError.message}</div>;
}
return <div>Error: {error.message}</div>;
}
return <div>{/* render data */}</div>;
}
全局错误处理
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
);
});
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
}
});
const httpLink = new HttpLink({
uri: 'https://api.example.com/graphql'
});
const client = new ApolloClient({
link: errorLink.concat(httpLink),
cache: new InMemoryCache()
});
7. 订阅
使用 useSubscription Hook
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_ADDED = gql`
subscription OnMessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
text
author {
name
}
createdAt
}
}
`;
function ChatRoom({ roomId }) {
const { data, loading, error } = useSubscription(MESSAGE_ADDED, {
variables: { roomId }
});
if (loading) return <div>Connecting...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h3>New Message:</h3>
<p>{data.messageAdded.text}</p>
<small>By {data.messageAdded.author.name}</small>
</div>
);
}
8. 性能优化
使用 persisted queries
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const persistedQueryLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true
});
const client = new ApolloClient({
link: persistedQueryLink.concat(httpLink),
cache: new InMemoryCache()
});
批量查询
import { useQuery, gql } from '@apollo/client';
const GET_MULTIPLE_USERS = gql`
query GetMultipleUsers($ids: [ID!]!) {
users(ids: $ids) {
id
name
email
}
}
`;
function UserList({ userIds }) {
const { loading, data } = useQuery(GET_MULTIPLE_USERS, {
variables: { ids: userIds }
});
if (loading) return <div>Loading...</div>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
9. 客户端开发最佳实践
| 实践 | 说明 |
|---|
| 使用缓存 | 减少网络请求,提高性能 |
| 实现乐观更新 | 提升用户体验 |
| 处理错误 | 提供友好的错误提示 |
| 使用分页 | 处理大量数据 |
| 实现加载状态 | 改善用户体验 |
| 使用订阅 | 实现实时更新 |
| 优化查询 | 只请求需要的字段 |
| 使用 persisted queries | 减少网络传输 |
| 实现重试机制 | 提高可靠性 |
| 监控性能 | 及时发现性能问题 |
10. 常见问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|
| 缓存未更新 | 缓存策略配置不当 | 调整缓存策略,手动更新缓存 |
| 查询慢 | 请求过多数据 | 优化查询,使用字段选择 |
| 内存占用高 | 缓存数据过多 | 定期清理缓存,限制缓存大小 |
| 订阅断开 | 网络不稳定 | 实现自动重连机制 |
| 错误处理不当 | 未正确处理错误 | 实现全局错误处理 |