状态管理与缓存策略


状态管理与缓存策略

为什么GraphQL需要客户端缓存

REST的缓存主要依赖HTTP协议——GET请求可以被浏览器、CDN、代理缓存。但GraphQL通常使用POST请求发送到单一端点,HTTP缓存无法直接工作。

这意味着GraphQL必须实现自己的缓存层。Apollo Client和Relay都内置了规范化缓存(Normalized Cache)——这是GraphQL客户端缓存的核心理念。

图:规范化缓存原理

图:规范化缓存原理

规范化缓存:按对象ID存储

规范化缓存的思路是:把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分页

缓存策略没有银弹——它需要根据数据的特性来选择。实时数据不应该缓存,静态数据应该长期缓存,半静态数据应该先缓存再更新。这个判断需要开发者对业务数据的时效性有清晰的理解。

缓存失效:GraphQL 客户端最难的问题

规范化缓存解决了"同一对象多处引用自动同步",但引入了一个新的难题:什么时候缓存该失效。服务端数据变了,客户端不知道。Apollo 的常见解法是"写后读"——Mutation 返回被修改的对象,客户端主动更新缓存,保证下次读到的就是新数据。但业务上常有 Mutation 不直接返回受影响列表的情况,比如"关注一个作者"并不会返回作者的全部帖子列表。

处理这类场景有几种成熟做法:

  1. refetchQueries:Mutation 完成后重查相关列表,简单直接,代价是多余的网络请求。
  2. cache.modify 定点更新:精确修改缓存中的某个字段,需要知道对象引用,耦合业务细节。
  3. 字段级无效化:给 Query 上的列表字段配 read 函数,配合服务端返回的版本号或时间戳判断是否需要重查。
  4. 轮询与订阅兜底:对时效性要求高的数据(未读数、在线状态)用轮询或 Subscription 推送,而不是依赖失效机制。
// 关注作者后,让"推荐列表"的缓存失效 const [followAuthor] = useMutation(FOLLOW_AUTHOR, { update(cache) { cache.evict({ fieldName: 'recommendedAuthors' }); cache.gc(); } }); ​

不要把缓存当作数据库。规范化缓存是内存级的,页面刷新就没了;它也不做服务端持久化。离线场景需要额外方案(Apollo 的 persistence 插件、Relay 的 offline store)。缓存的角色是"同一会话内减少重复请求、保持 UI 一致",不是替代后端存储。

实战:列表增删改的缓存套路

一个典型的帖子管理页,涉及三类操作:

  • 新增帖子:调用 createPost 后,把新对象插入列表缓存(由 read/merge 函数处理),或用 refetchQueries 重查列表。
  • 删除帖子:cache.evict 删除对象,再 gc 触发列表重算;如果列表字段有 merge 函数,删除后要同步从列表里移除引用。
  • 修改帖子:cache.modify 定点更新该对象字段,所有引用它的查询自动更新——这是规范化缓存最大的收益场景。

把这三类操作封装成自定义 hook,页面组件只调用 hook 不关心缓存细节,是生产项目里最常见的组织方式。

作者与出处
原作者:灏天文库
来源:平台策划编纂
整理人: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U