关于 apollo-graphql
apollo-graphql 汇集了使用 Apollo Client、TypeScript 和 React 构建 GraphQL 应用的实践指南,内容涵盖查询与变更、缓存、分页、错误处理及客户端状态管理。
主要功能
- 01Apollo Client 配置与 hooks
- 02可复用的查询片段
- 03类型安全的 GraphQL 操作
- 04缓存策略与分页
- 05批处理和 DataLoader 指南
- 06查询与变更错误处理
适用场景
- 01组织 GraphQL 查询和变更
- 02构建类型安全的 React 数据 hooks
- 03用游标分页处理大型结果集
- 04减少重复的后端请求
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill apollo-graphql安装后在 Claude Code 中输入/apollo-graphql确认已加载。
详细介绍
Apollo GraphQL 最佳实践
你是 Apollo Client、GraphQL、TypeScript 和 React 开发方面的专家。Apollo Client 为 GraphQL 应用提供全面的状态管理解决方案,支持智能缓存、乐观 UI 更新,并可与 React 无缝集成。
核心原则
- 使用 Apollo Client 进行状态管理和数据获取
- 使用查询组件获取数据
- 使用变更操作修改数据
- 使用片段复用查询部分
- 妥善处理错误和加载状态
- 利用 TypeScript 为 GraphQL 操作提供类型安全保障
项目结构
src/
components/
graphql/
queries/
users.ts
posts.ts
mutations/
users.ts
posts.ts
fragments/
user.ts
post.ts
hooks/
useUser.ts
usePosts.ts
pages/
utils/
apollo-client.ts
types/
generated/ # Generated TypeScript types
设置和配置
Apollo Client 设置
// utils/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
const httpLink = new HttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT,
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path }) => {
console.error(`[GraphQL error]: Message: ${message}, Path: ${path}`);
});
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
}
});
export const apolloClient = new ApolloClient({
link: from([errorLink, httpLink]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'cache-first',
errorPolicy: 'all',
},
mutate: {
errorPolicy: 'all',
},
},
});
Apollo Provider 设置
// pages/_app.tsx or app/providers.tsx
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from '@/utils/apollo-client';
function App({ children }: { children: React.ReactNode }) {
return (
<ApolloProvider client={apolloClient}>
{children}
</ApolloProvider>
);
}
Schema 设计最佳实践
命名约定
为类型、字段和参数使用描述性名称:
# Good
type User {
id: ID!
firstName: String!
lastName: String!
emailAddress: String!
createdAt: DateTime!
}
type Query {
getUserById(id: ID!): User
getUsersByRole(role: UserRole!): [User!]!
}
# Avoid
type Query {
getUser(id: ID!): User # Less descriptive
}
Schema 结构
定义清晰的 Schema,以反映业务领域:
type Query {
user(id: ID!): User
users(first: Int, after: String, filter: UserFilter): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
}
input CreateUserInput {
firstName: String!
lastName: String!
email: String!
}
type CreateUserPayload {
user: User
errors: [UserError!]
}
查询模式
使用片段定义查询
// graphql/fragments/user.ts
import { gql } from '@apollo/client';
export const USER_FIELDS = gql`
fragment UserFields on User {
id
firstName
lastName
email
avatar
createdAt
}
`;
// graphql/queries/users.ts
import { gql } from '@apollo/client';
import { USER_FIELDS } from '../fragments/user';
export const GET_USER = gql`
${USER_FIELDS}
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
}
}
`;
export const GET_USERS = gql`
${USER_FIELDS}
query GetUsers($first: Int, $after: String) {
users(first: $first, after: $after) {
edges {
node {
...UserFields
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
自定义查询 Hooks
// hooks/useUser.ts
import { useQuery, QueryHookOptions } from '@apollo/client';
import { GET_USER } from '@/graphql/queries/users';
import { User, GetUserQuery, GetUserQueryVariables } from '@/types/generated';
export function useUser(
id: string,
options?: QueryHookOptions<GetUserQuery, GetUserQueryVariables>
) {
const { data, loading, error, refetch } = useQuery<
GetUserQuery,
GetUserQueryVariables
>(GET_USER, {
variables: { id },
skip: !id,
...options,
});
return {
user: data?.user,
loading,
error,
refetch,
};
}
变更模式
定义变更操作
// graphql/mutations/users.ts
import { gql } from '@apollo/client';
import { USER_FIELDS } from '../fragments/user';
export const CREATE_USER = gql`
${USER_FIELDS}
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
user {
...UserFields
}
errors {
field
message
}
}
}
`;
export const UPDATE_USER = gql`
${USER_FIELDS}
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
user {
...UserFields
}
errors {
field
message
}
}
}
`;
自定义变更 Hooks
// hooks/useCreateUser.ts
import { useMutation, MutationHookOptions } from '@apollo/client';
import { CREATE_USER } from '@/graphql/mutations/users';
import { GET_USERS } from '@/graphql/queries/users';
export function useCreateUser(options?: MutationHookOptions) {
const [createUser, { data, loading, error }] = useMutation(CREATE_USER, {
refetchQueries: [{ query: GET_USERS }],
onError: (error) => {
console.error('Failed to create user:', error);
},
...options,
});
return {
createUser: (input: CreateUserInput) => createUser({ variables: { input } }),
data,
loading,
error,
};
}
乐观更新
function useUpdateUser() {
const [updateUser] = useMutation(UPDATE_USER, {
optimisticResponse: ({ id, input }) => ({
__typename: 'Mutation',
updateUser: {
__typename: 'UpdateUserPayload',
user: {
__typename: 'User',
id,
...input,
},
errors: null,
},
}),
update: (cache, { data }) => {
const updatedUser = data?.updateUser?.user;
if (updatedUser) {
cache.modify({
id: cache.identify(updatedUser),
fields: {
firstName: () => updatedUser.firstName,
lastName: () => updatedUser.lastName,
},
});
}
},
});
return { updateUser };
}
缓存策略
缓存规范化
const cache = new InMemoryCache({
typePolicies: {
User: {
keyFields: ['id'],
},
Post: {
keyFields: ['id'],
fields: {
author: {
merge: true,
},
},
},
},
});
读取和写入缓存
// Read from cache
const user = client.readFragment({
id: `User:${userId}`,
fragment: USER_FIELDS,
});
// Write to cache
client.writeFragment({
id: `User:${userId}`,
fragment: USER_FIELDS,
data: {
...user,
firstName: 'Updated Name',
},
});
分页
基于游标的分页(Relay 风格)
对于大型或快速变化的数据,建议使用基于游标的分页:
function useInfiniteUsers() {
const { data, loading, fetchMore } = useQuery(GET_USERS, {
variables: { first: 10 },
});
const loadMore = () => {
if (!data?.users.pageInfo.hasNextPage) return;
fetchMore({
variables: {
after: data.users.pageInfo.endCursor,
},
});
};
return {
users: data?.users.edges.map((edge) => edge.node) ?? [],
loading,
hasMore: data?.users.pageInfo.hasNextPage ?? false,
loadMore,
};
}
分页的缓存合并策略
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
keyArgs: ['filter'],
merge(existing = { edges: [] }, incoming) {
return {
...incoming,
edges: [...existing.edges, ...incoming.edges],
};
},
},
},
},
},
});
性能优化
DataLoader 模式
使用批处理技术减少后端请求:
// Server-side with DataLoader
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (ids: string[]) => {
const users = await db.users.findMany({ where: { id: { in: ids } } });
return ids.map((id) => users.find((u) => u.id === id));
});
// In resolver
const resolvers = {
Post: {
author: (post) => userLoader.load(post.authorId),
},
};
查询批处理
import { BatchHttpLink } from '@apollo/client/link/batch-http';
const batchLink = new BatchHttpLink({
uri: '/graphql',
batchMax: 10,
batchInterval: 20,
});
获取策略
// Network only - skip cache
useQuery(GET_USER, {
fetchPolicy: 'network-only',
});
// Cache first - prefer cache
useQuery(GET_USER, {
fetchPolicy: 'cache-first',
});
// Cache and network - return cache, then update
useQuery(GET_USER, {
fetchPolicy: 'cache-and-network',
});
错误处理
查询错误处理
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useUser(userId);
if (loading) return <Skeleton />;
if (error) {
return (
<ErrorMessage
message="Failed to load user profile"
retry={() => refetch()}
/>
);
}
return <ProfileCard user={data} />;
}
变更错误处理
function CreateUserForm() {
const { createUser, loading, error } = useCreateUser({
onCompleted: (data) => {
if (data.createUser.errors?.length) {
// Handle validation errors
data.createUser.errors.forEach((err) => {
setFieldError(err.field, err.message);
});
} else {
// Success
toast.success('User created successfully');
}
},
});
// ...
}
状态管理
对于简单的状态需求,使用 Apollo Client 的本地状态管理:
// Define local-only fields
const typeDefs = gql`
extend type Query {
isLoggedIn: Boolean!
cartItems: [CartItem!]!
}
`;
// Read local state
const IS_LOGGED_IN = gql`
query IsLoggedIn {
isLoggedIn @client
}
`;
// Write local state
client.writeQuery({
query: IS_LOGGED_IN,
data: { isLoggedIn: true },
});
对于复杂的客户端状态,可考虑在 Apollo 旁使用 Zustand 或 Redux Toolkit。
应避免的反模式
- 过度获取/获取不足:仅请求所需字段
- 过于频繁的 API 调用:通过批处理和 DataLoader 尽量减少往返请求
- 上帝对象:避免包含过多字段的大型单体类型
- 缺少错误处理:始终在查询和变更操作层面处理错误
- 忽视缓存:利用 Apollo 的缓存提升性能
- 不使用片段:片段可提升复用性和可维护性
- 跳过 TypeScript:根据 Schema 生成类型,以确保类型安全
关键约定
- 在应用根目录使用 Apollo Provider
- 为 Apollo 操作实现自定义 Hooks
- 使用 TypeScript 为 GraphQL 操作提供类型安全保障(生成类型)
- 将查询、变更操作和片段分别放在不同文件中
- 使用片段复用查询部分
- 妥善处理错误和加载状态
- 对大型数据集使用基于游标的分页
- 利用 DataLoader 高效加载数据
评论
支持 Markdown