GraphQL 客户端开发有哪些关键要点
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 Hookimport { 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 queryimport { 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 Hookimport { 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 Hookimport { 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 queriesimport { 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. 常见问题及解决方案| 问题 | 原因 | 解决方案 ||------|------|----------|| 缓存未更新 | 缓存策略配置不当 | 调整缓存策略,手动更新缓存 || 查询慢 | 请求过多数据 | 优化查询,使用字段选择 || 内存占用高 | 缓存数据过多 | 定期清理缓存,限制缓存大小 || 订阅断开 | 网络不稳定 | 实现自动重连机制 || 错误处理不当 | 未正确处理错误 | 实现全局错误处理 |