mcpField
A
apollo-graphql

apollo-graphql

github.com/mindrally开发287681 次安装3 周前更新
代码仓库

关于 apollo-graphql

apollo-graphql 汇集了使用 Apollo Client、TypeScript 和 React 构建 GraphQL 应用的实践指南,内容涵盖查询与变更、缓存、分页、错误处理及客户端状态管理。

主要功能

  1. 01Apollo Client 配置与 hooks
  2. 02可复用的查询片段
  3. 03类型安全的 GraphQL 操作
  4. 04缓存策略与分页
  5. 05批处理和 DataLoader 指南
  6. 06查询与变更错误处理

适用场景

  1. 01组织 GraphQL 查询和变更
  2. 02构建类型安全的 React 数据 hooks
  3. 03用游标分页处理大型结果集
  4. 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 生成类型,以确保类型安全

关键约定

  1. 在应用根目录使用 Apollo Provider
  2. 为 Apollo 操作实现自定义 Hooks
  3. 使用 TypeScript 为 GraphQL 操作提供类型安全保障(生成类型)
  4. 将查询、变更操作和片段分别放在不同文件中
  5. 使用片段复用查询部分
  6. 妥善处理错误和加载状态
  7. 对大型数据集使用基于游标的分页
  8. 利用 DataLoader 高效加载数据

评论

支持 Markdown