前端 GraphQL 客户端集成:Apollo Client、Relay 与 urql 选型

前端 GraphQL 客户端集成:Apollo Client(缓存策略/链路中间件/局部状态/乐观更新)、Relay(编译时优化/Fragment colocation/连接分页/refetchContainer)、urql(可扩展的缓存交换层/轻量快速)、缓存架构对比(规范化缓存/文档缓存/网络缓存)、订阅与实时更新(WebSocket/Server-Sent Events)、错误处理与重试策略、TypeScript 类型生成(CodeGen/GraphQL Code Generator)、文件上传与多操作批量、性能优化(数据预取/分页游标/持久化缓存)。

引言

GraphQL 改变了前后端的协作方式——客户端声明所需数据,服务端精确返回。但 GraphQL 的真正威力在前端客户端:规范化缓存让同一份数据在不同查询间自动同步、乐观更新让 UI 零延迟响应用户操作、订阅让实时数据自动流入。Apollo Client、Relay 和 urql 是三个主流选择,各有其设计哲学。本文从缓存架构出发,覆盖查询/突变/订阅、分页策略、TypeScript 集成和性能优化——给 GraphQL 前端集成一份完整的决策地图。

前置:前端性能优化基础


一、三大客户端选型

1.1 对比矩阵

维度Apollo ClientRelayurql
体积~30KB~20KB~8KB
缓存规范化(强大)规范化(自动)可扩展(简单默认)
TypeScript优秀需编译良好
学习曲线中等陡峭低
生态最丰富Facebook 级增长中
适用规模中小型→大型大型(Meta 级)小型→中型

1.2 选择策略

Apollo Client:通用选择,生态最强,缓存灵活,团队熟悉度高
Relay:超大规模应用,编译时优化,Fragment colocation 强制最佳实践
urql:轻量快速,Prisma/Modulz 出品,可插拔架构,适合新项目
# 建议:除非有 Meta 级别规模,否则 Apollo Client 或 urql

二、Apollo Client:缓存与链路

2.1 基础配置

import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';

const client = new ApolloClient({
  link: new HttpLink({ uri: '/graphql' }),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            keyArgs: ['filter'],
            merge(existing = [], incoming) {
              return [...existing, ...incoming];
            }
          }
        }
      }
    }
  })
});

2.2 规范化缓存

InMemoryCache 自动将查询结果扁平化为「对象图」:
  posts: [{ id: "1", title: "A" }, { id: "2", title: "B" }]
→ 缓存为:
  Post:1 = { id: "1", title: "A" }
  Post:2 = { id: "2", title: "B" }
  ROOT_QUERY.posts = [{ __ref: "Post:1" }, { __ref: "Post:2" }]

# 好处:不同查询引用同一对象时自动同步更新

2.3 查询与突变

import { useQuery, useMutation, gql } from '@apollo/client';

const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      title
      author {
        name
      }
    }
  }
`;

const CREATE_POST = gql`
  mutation CreatePost($input: PostInput!) {
    createPost(input: $input) {
      id
      title
    }
  }
`;

function Posts() {
  const { data, loading, error } = useQuery(GET_POSTS);
  const [createPost] = useMutation(CREATE_POST, {
    update(cache, { data: { createPost } }) {
      cache.modify({
        fields: {
          posts(existingPosts = []) {
            return [...existingPosts, createPost];
          }
        }
      });
    }
  });

  if (loading) return <Loading />;
  if (error) return <Error message={error.message} />;

  return (
    <div>
      {data.posts.map(post => <PostCard key={post.id} post={post} />)}
      <button onClick={() => createPost({ variables: { input: { title: 'New' } } })}>
        Add Post
      </button>
    </div>
  );
}

2.4 乐观更新

const [likePost] = useMutation(LIKE_POST, {
  optimisticResponse: (vars) => ({
    likePost: {
      id: vars.postId,
      likes: data.post.likes + 1,
      __typename: 'Post'
    }
  }),
  update(cache, result) {
    cache.writeFragment({
      id: `Post:${vars.postId}`,
      fragment: gql`fragment PostLikes on Post { likes }`,
      data: result.data.likePost
    });
  }
});

三、Relay:编译时优化与 Fragment Colocation

3.1 Relay 理念

Fragment Colocation:组件声明自己的数据需求
编译时优化:GraphQL 查询在构建时编译为可执行代码
# 每个组件只声明自己需要的字段,Relay 自动合并查询

3.2 Fragment 定义

import { graphql, useFragment } from 'react-relay';

const PostCardFragment = graphql`
  fragment PostCard_post on Post {
    id
    title
    author {
      name
      avatar
    }
  }
`;

function PostCard({ post }: { post: PostCard_post$key }) {
  const data = useFragment(PostCardFragment, post);
  
  return (
    <div>
      <h3>{data.title}</h3>
      <Author name={data.author.name} avatar={data.author.avatar} />
    </div>
  );
}

3.3 连接分页

const PostsQuery = graphql`
  query PostsQuery($count: Int!, $cursor: String) {
    posts(first: $count, after: $cursor) @connection(key: "Posts_posts") {
      edges {
        node {
          id
          ...PostCard_post
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

function Posts() {
  const { data, loadNext, hasNext } = usePaginationFragment(
    PostsQuery,
    posts
  );

  return (
    <div>
      {data.posts.edges.map(edge => (
        <PostCard key={edge.node.id} post={edge.node} />
      ))}
      {hasNext && <button onClick={() => loadNext(10)}>Load More</button>}
    </div>
  );
}

四、urql:可扩展的轻量方案

4.1 基础用法

import { createClient, Provider, useQuery } from 'urql';

const client = createClient({
  url: '/graphql',
  exchanges: [dedupExchange, cacheExchange, fetchExchange]
});

function Posts() {
  const [result] = useQuery({ query: GET_POSTS });
  const { data, fetching, error } = result;

  if (fetching) return <Loading />;
  if (error) return <Error message={error.message} />;

  return <PostList posts={data.posts} />;
}

4.2 自定义 Exchange(中间件)

import { Exchange, Operation } from '@urql/core';

const authExchange: Exchange = ({ forward }) => (ops$) => {
  return pipe(
    ops$,
    map((operation: Operation) => {
      const token = localStorage.getItem('token');
      return makeOperation(operation.kind, operation, {
        ...operation.context,
        fetchOptions: {
          headers: { Authorization: token ? `Bearer ${token}` : '' }
        }
      });
    }),
    forward
  );
};

const client = createClient({
  url: '/graphql',
  exchanges: [dedupExchange, cacheExchange, authExchange, fetchExchange]
});

五、缓存架构对比

5.1 三种缓存策略

策略ApolloRelayurql适用
规范化缓存✅ 默认✅ 强制✅ 可选数据关联复杂
文档缓存❌❌✅ 默认简单查询独立
网络层缓存✅ HTTP✅ HTTP✅ HTTPAPI 层缓存

5.2 缓存更新策略

乐观更新:UI 先更新,API 后确认(mutation 时)
refetchQueries:突变后重查询相关查询
update:手动修改缓存
订阅更新:实时数据自动流入缓存

六、订阅与实时更新

6.1 WebSocket 订阅

import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

const wsLink = new GraphQLWsLink(createClient({
  url: 'wss://api.example.com/graphql'
}));

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
  },
  wsLink,
  httpLink
);

6.2 使用订阅

const COMMENTS_SUBSCRIPTION = gql`
  subscription OnCommentAdded($postId: ID!) {
    commentAdded(postId: $postId) {
      id
      content
      author {
        name
      }
    }
  }
`;

function Comments({ postId }) {
  const { data } = useSubscription(COMMENTS_SUBSCRIPTION, { variables: { postId } });
  
  return (
    <div>
      {data?.commentAdded && <Comment comment={data.commentAdded} />}
    </div>
  );
}

七、TypeScript 类型生成

7.1 GraphQL Code Generator

# codegen.yml
schema: ./schema.graphql
generates:
  ./src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHooks: true
      withHOC: false
      withComponent: false

7.2 使用生成类型

import { useGetPostsQuery, useCreatePostMutation } from './generated/graphql';

function Posts() {
  const { data } = useGetPostsQuery();  // 完全类型安全
  const [createPost] = useCreatePostMutation();
}

八、性能优化

8.1 数据预取

// 路由切换前预取
function PostLink({ postId }) {
  const [prefetch] = useLazyQuery(GET_POST);

  return (
    <Link
      to={`/posts/${postId}`}
      onMouseEnter={() => prefetch({ variables: { id: postId } })}
    >
      {postTitle}
    </Link>
  );
}

8.2 分页策略

Offset-based:简单但慢(大数据偏移)
Cursor-based:稳定高效(Relay 推荐)
Connection Spec:GraphQL Cursor Connections 规范
# Relay 的 @connection 指令自动处理分页缓存

8.3 持久化缓存

// Apollo 持久化缓存
import { persistCache } from 'apollo3-cache-persist';

persistCache({
  cache,
  storage: window.localStorage,
  maxSize: 1048576  // 1MB
});

结语

GraphQL 客户端选型是「团队规模」和「应用复杂度」的函数:Apollo Client 是通用选择,缓存强大生态丰富;Relay 适合超大规模,编译时优化和 Fragment colocation 强制最佳实践;urql 轻量可扩展,适合新项目和小团队。无论选哪个,核心掌握点都是缓存——规范化缓存让数据自动同步,乐观更新让 UI 即时响应,订阅让实时数据自然流入。TypeScript 类型生成让 GraphQL 的类型安全从服务端延伸到前端。GraphQL 不是 REST 的替代,而是在需要精确数据获取、强类型、实时更新场景下的更好选择。而客户端缓存,正是 GraphQL 在前端真正发挥威力的地方。


一句话记忆:GraphQL 客户端选型——Apollo Client(通用最强/30KB/规范化缓存/乐观更新/生态丰富)、Relay(超大规模/编译时优化/Fragment colocation/自动分页)、urql(轻量 8KB/可插拔 exchange);缓存核心——规范化缓存扁平化对象图让多查询自动同步、乐观更新 UI 先变 API 后确认、update/refetchQueries/订阅更新多策略;订阅用 graphql-ws WebSocket、实时数据自动流入;TypeScript 用 GraphQL Code Generator 自动生成 hooks 类型;性能靠预取(hover 时 load)、cursor 分页(稳定高效)、持久化缓存(localStorage/apollo3-cache-persist);Relay @connection 自动处理分页缓存——「GraphQL 的魔力在客户端缓存,选型看规模、掌握缓存是核心」。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. 前端性能调试实战:从首屏加载到运行时瓶颈
  2. 前端表单与验证架构:设计模式、状态管理与无障碍
  3. 前端与移动端 RUM:Web Vitals、会话与用户体验监控