REST的缓存主要依赖HTTP协议——GET请求可以被浏览器、CDN、代理缓存。但GraphQL通常使用POST请求发送到单一端点,HTTP缓存无法直接工作。
这意味着GraphQL必须实现自己的缓存层。Apollo Client和Relay都内置了规范化缓存(Normalized Cache)——这是GraphQL客户端缓存的核心理念。

规范化缓存的思路是:把GraphQL返回的嵌套JSON拆解成扁平的对象存储,每个对象用 __typename + id 作为唯一键。
查询返回:
{ "user": { "__typename": "User", "id": "1001", "name": "林远", "posts": [ { "__typename": "Post", "id": "2001", "title": "GraphQL笔记", "author": { "__typename": "User", "id": "1001", "name": "林远" } } ] } }
缓存存储(扁平化):
User:1001 → { name: "林远" } Post:2001 → { title: "GraphQL笔记" }
注意 author 字段没有存储完整的用户对象——因为缓存中已经有 User:1001,直接引用即可。这就是"规范化"的含义:避免同一对象在缓存中重复存储。
规范化的好处:当Mutation更新了 Post:2001 的 title 时,所有引用这个帖子的查询都能自动更新——因为它们指向缓存中的同一个对象。
Apollo Client需要知道如何为每个对象生成缓存键。默认使用 __typename + id 字段。如果对象类型使用其他ID字段(比如 _id),需要配置:
const cache = new InMemoryCache({ typePolicies: { User: { keyFields: ['_id'], // 用 _id 代替 id fields: { posts: { // 列表字段用参数作为缓存键的一部分 keyArgs: ['limit', 'offset'], // 自定义合并函数 merge(existing = [], incoming) { return [...existing, ...incoming]; } } } }, Post: { keyFields: ['id'], }, // 没有ID的类型 ProductCategory: { keyFields: false, // 不缓存 } } });
typePolicies 的配置决定了缓存的行为:
keyFields:哪些字段组合作为缓存键fields.merge:列表字段收到新数据时如何与旧数据合并fields.keyArgs:哪些参数参与缓存键计算Mutation执行后需要更新缓存。Apollo Client提供了三种方式:
自动更新:如果Mutation返回的修改对象与缓存中的对象类型相同,Apollo会自动合并更新。
const [updateProfile] = useMutation(UPDATE_PROFILE, { // update回调手动更新缓存 update(cache, { data: { updateProfile } }) { cache.modify({ id: cache.identify(updateProfile), fields: { name: () => updateProfile.name, bio: () => updateProfile.bio, } }); } });
cache.evict:删除缓存中的某个对象或字段:
const [deletePost] = useMutation(DELETE_POST, { update(cache, { data: { deletePost: postId } }) { cache.evict({ id: `Post:${postId}` }); // 通知缓存失效,触发相关查询重新获取 cache.gc(); } });
refetchQueries:执行Mutation后重新获取指定查询:
const [createPost] = useMutation(CREATE_POST, { refetchQueries: [{ query: GET_POSTS, variables: { limit: 10 } }], });
Apollo Client的缓存不仅用于远程数据,也可以管理本地状态(不需要通过GraphQL查询获取的数据):
// 初始化本地状态 const cache = new InMemoryCache({ typePolicies: { Query: { fields: { isLoggedIn: { read() { return localStorage.getItem('token') !== null; } }, theme: { read() { return 'dark'; }, write(_, value) { document.documentElement.className = value; } } } } } }); // 使用本地状态 const { data } = useQuery(gql` query LocalState { isLoggedIn @client theme @client } `); const [setTheme] = useMutation(gql` mutation SetTheme($theme: String!) { setTheme(theme: $theme) @client } `);
@client 指令告诉Apollo Client这个字段从本地缓存读取,不发网络请求。这种方式把远程数据和本地状态统一在同一个缓存体系中管理,避免了Redux等额外状态管理库的引入。
| 场景 | 推荐策略 |
|---|---|
| 实时数据(聊天消息、股票价格) | network-only 或 no-cache |
| 用户个人资料 | cache-first,Mutation后更新 |
| 列表数据(帖子列表、搜索结果) | cache-and-network,带合并函数 |
| 静态配置(枚举、字典) | cache-first,长TTL |
| 分页数据 | cache-and-network,cursor-based分页 |
缓存策略没有银弹——它需要根据数据的特性来选择。实时数据不应该缓存,静态数据应该长期缓存,半静态数据应该先缓存再更新。这个判断需要开发者对业务数据的时效性有清晰的理解。
规范化缓存解决了"同一对象多处引用自动同步",但引入了一个新的难题:什么时候缓存该失效。服务端数据变了,客户端不知道。Apollo 的常见解法是"写后读"——Mutation 返回被修改的对象,客户端主动更新缓存,保证下次读到的就是新数据。但业务上常有 Mutation 不直接返回受影响列表的情况,比如"关注一个作者"并不会返回作者的全部帖子列表。
处理这类场景有几种成熟做法:
// 关注作者后,让"推荐列表"的缓存失效 const [followAuthor] = useMutation(FOLLOW_AUTHOR, { update(cache) { cache.evict({ fieldName: 'recommendedAuthors' }); cache.gc(); } });
不要把缓存当作数据库。规范化缓存是内存级的,页面刷新就没了;它也不做服务端持久化。离线场景需要额外方案(Apollo 的 persistence 插件、Relay 的 offline store)。缓存的角色是"同一会话内减少重复请求、保持 UI 一致",不是替代后端存储。
一个典型的帖子管理页,涉及三类操作:
把这三类操作封装成自定义 hook,页面组件只调用 hook 不关心缓存细节,是生产项目里最常见的组织方式。