引言
GraphQL 改变了前后端的协作方式——客户端声明所需数据,服务端精确返回。但 GraphQL 的真正威力在前端客户端:规范化缓存让同一份数据在不同查询间自动同步、乐观更新让 UI 零延迟响应用户操作、订阅让实时数据自动流入。Apollo Client、Relay 和 urql 是三个主流选择,各有其设计哲学。本文从缓存架构出发,覆盖查询/突变/订阅、分页策略、TypeScript 集成和性能优化——给 GraphQL 前端集成一份完整的决策地图。
前置:前端性能优化基础
一、三大客户端选型
1.1 对比矩阵
| 维度 | Apollo Client | Relay | urql |
|---|---|---|---|
| 体积 | ~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 三种缓存策略
| 策略 | Apollo | Relay | urql | 适用 |
|---|---|---|---|---|
| 规范化缓存 | ✅ 默认 | ✅ 强制 | ✅ 可选 | 数据关联复杂 |
| 文档缓存 | ❌ | ❌ | ✅ 默认简单 | 查询独立 |
| 网络层缓存 | ✅ HTTP | ✅ HTTP | ✅ HTTP | API 层缓存 |
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 的魔力在客户端缓存,选型看规模、掌握缓存是核心」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。