1. 入门概念与目标
1.1 缓存的核心作用
在 GraphQL 的客户端数据管理场景中,Apollo Client 的缓存扮演着“本地数据镜像”的角色,它能让应用快速响应用户操作,同时尽量减少对服务器的请求。通过对 InMemoryCache 的正确配置,开发者能够实现更高的缓存命中率和更低的网络带宽消耗。
理解缓存的定位有助于设计更合理的查询策略:哪些数据应放在缓存中、如何避免数据冗余、以及何时应优先读取缓存而非直接请求网络。
1.2 缓存与网络的协同关系
缓存并不是对服务端数据的替代,而是一个与网络协同工作的层次。通过设置合适的 fetchPolicy,可以在不同场景下权衡缓存与网络的优先级,提升页面加载速度,同时确保用户看到的数字尽可能新近。
本章的核心在于建立对 缓存-网络交互的直觉,为后续章节的配置和调优打下基础,你将看到如何在实际项目中实现从入门到实战的缓存操作。
2. InMemoryCache 的基本配置
2.1 初始配置与默认行为
InMemoryCache是 Apollo Client 的默认缓存实现,默认行为通常已经足够应对基础的查询与缓存写入需求。了解它的结构有助于你更好地定制数据写入和读取路径。
通过在创建 ApolloClient 时传入 cache: new InMemoryCache(),你可以为整个应用建立一个统一的缓存命名空间,从而实现跨组件的共享数据。
import { ApolloClient, InMemoryCache } from '@apollo/client';const client = new ApolloClient({uri: 'https://api.example.com/graphql',cache: new InMemoryCache()
});
2.2 初始缓存结构的观测
在开发阶段,使用浏览器开发者工具查看缓存的结构,可以帮助你理解数据是如何被写入到 缓存栈中的。通过直观的可视化,你可以找到潜在的重复数据或无用字段。
命中率分析也是提升性能的重要手段,结合实际查询的 字段选择、查询粒度,可以显著降低不必要的数据传输。
3. typePolicies 与字段策略
3.1 typePolicies 的作用
typePolicies 提供了对对象类型及其字段的全局行为控制,允许你定义数据的唯一标识、字段的读写逻辑以及合并策略。这是实现复杂缓存场景的核心能力。

通过配置 typePolicies,你可以让同一字段在不同查询场景下保持一致性,避免数据冲突和不一致的 UI 展示。
const client = new ApolloClient({cache: new InMemoryCache({typePolicies: {Query: {fields: {books: {// 不同查询的合并策略keyArgs: false}}}}})
});
3.2 fieldPolicy 与 read/write 与 merge
在 fieldPolicy 中可以为字段定义自定义的 read 与 merge 行为,控制从缓存中读取数据时的加工和写入缓存时的数据拼接。
通过细粒度的 fieldPolicy,你可以实现跨分页、分组等复杂场景下的缓存再利用。
const client = new ApolloClient({cache: new InMemoryCache({typePolicies: {User: {keyFields: ['id'],fields: {posts: {// 自定义合并逻辑,可实现分页无缝拼接merge(existing = [], incoming) {return [...existing, ...incoming];}}}}}})
});
4. 自定义合并策略(merge)
4.1 列表分页合并
在实现分页加载时,merge 函数可以把服务器返回的新页面数据与本地已有数据进行拼接,从而形成一个连续的结果集,避免重复请求或覆盖现有数据。
关键点是处理重复项与排序边界,确保新数据不覆盖老数据且保持正确的排序。
const client = new ApolloClient({cache: new InMemoryCache({typePolicies: {Query: {fields: {feed: {keyArgs: false,merge(existing = [], incoming, { args }) {const merged = existing ? existing.slice(0) : [];// 假设分页参数有 offsetconst offset = args?.offset ?? 0;for (let i = 0; i < incoming.length; ++i) {merged[offset + i] = incoming[i];}return merged;}}}}}})
});
4.2 对象字段合并
除了列表,对象字段也可以通过 merge 实现更细粒度的更新,例如在一个对象中的子字段发生变化时只更新相应的部分。
在复杂对象模型中,谨慎设计合并规则,避免对现有字段造成意外覆盖。
merge(existing = {}, incoming) {return { ...existing, ...incoming };
}
5. 数据唯一标识 keyFields
5.1 如何设置 primary key
为每个实体指定唯一标识,是缓存正确工作的前提。通过 typePolicies 的 keyFields,你可以定义一个或多个字段来唯一标识数据,例如 Book 的 id。
正确的唯一键可避免不同对象之间的键冲突,确保查询结果被正确地写入和读取。
typePolicies: {Book: {keyFields: ['id'] // 将 id 作为该类型的唯一标识}
}
5.2 使用 keyArgs 控制缓存分区
keyArgs 控制哪些查询参数会影响缓存分区。将其设置为 false 可以让同一个字段在不同参数下共享缓存条目,或者通过自定义分区策略实现更复杂的缓存命中逻辑。
这种做法有助于提升缓存命中率,尤其是在存在多组类似查询但参数略有差异的场景中。
6. 查询缓存读取策略
6.1 fetchPolicy 的选取
fetchPolicy 决定查询优先级:cache-first、network-only、cache-first、cache-and-network 等。正确选择能显著影响加载体验与数据时效。
在编辑/创建数据后,使用 cache-and-network 可以先展示缓存结果,随后从网络获取最新内容,兼顾速度与新鲜度。
useQuery(GET_BOOKS, {fetchPolicy: 'cache-first' // 优先命中缓存,若未命中再请求网络
});
6.2 cache-only、network-only 的适用场景
cache-only 适合只读取本地缓存的场景,避免任何网络请求;network-only 适合需要确保数据最新的场景,即使缓存有数据也忽略它。
在离线模式或性能敏感场景中,合理混合实现可以提升用户体验,同时降低对网络的依赖。
7. 数据归并与 Fragment
7.1 Fragment 与缓存写入
使用 fragment 可以解耦查询结构和缓存结构,帮助你在不同组件之间共享数据片段。通过 Fragment,缓存写入变得更可控,减少冗余字段。
在实现复杂界面时,Fragment 匹配与 read、merge 的组合可以提升缓存命中率并降低网络负载。
gql`fragment BookDetails on Book {idtitleauthor { id name }}
`;
7.2 使用 cache.modify 进行局部更新
当你需要对缓存中的特定字段进行原子更新时,cache.modify 提供了粒度控制能力,避免全量重写缓存。
通过对指定字段应用变更,你可以实现乐观更新、局部刷新等高效的 UI 更新模式。
cache.modify({id: cache.identify({ __typename: 'Book', id: '123' }),fields: {title(existing) {return existing + ' (Updated)';}}
});
8. 乐观更新与缓存同步
8.1 乐观响应的实现
乐观更新是在发起 mutation 之前就对 UI 作出假设性变更,cache.writeFragment 或 cache.modify 可以实现快速呈现,提升用户体验。
实现时要考虑回滚策略:如果服务器返回错误,需要将缓存回滚到变更前的状态,确保 UI 的一致性。
const optimisticBook = { __typename: 'Book', id: '123', title: '新书标题' };
cache.writeFragment({id: cache.identify(optimisticBook),fragment: gql`fragment BookTitle on Book { title }`,data: { title: '新书标题' }
});
8.2 与变更冲突的处理
乐观更新可能引发冲突,typePolicies 与 merge 的配合是解决冲突的基础。确保服务器最终状态与缓存状态保持一致是关键。
在冲突出现时,使用 refetchQueries 或页面级的重新查询可以把 UI 拉回到服务器一致的状态。
9. 调试与性能优化
9.1 开发工具与调试
调试缓存问题时,浏览器中的 GraphQL 客户端扩展和 Apollo DevTools 能帮助你直观地查看 缓存命中情况、字段策略、以及 merging 过程。
利用日志或自定义中间件,记录 缓存写入 与 读取 的关键阶段,能够快速定位问题来源。
// 简单的缓存写入日志示例
cache.writeQuery({ query: GET_BOOKS, data: { books: [...] } });
console.log('Cache updated for GET_BOOKS');
9.2 性能评估与最佳实践
性能优化的核心在于最小化缓存的无效写入、减少冗余查询、提高命中率。分区键、字段策略、以及合并逻辑的正确组合能带来显著的性能提升。
实践中,建议从小范围开始变更,逐步扩展到整个应用,以避免全局性副作用。
10. 实战场景示例
10.1 实战场景:博客列表的分页缓存
在一个以博客文章为核心的数据模型中,使用 merge 与 keyArgs 的组合来实现分页数据的无缝拼接,确保用户在翻页时不会重复加载。
通过将 Feed 的查询字段配置为 keyArgs: false,可以让不同分页参数的请求命中同一缓存条目,从而实现更高的缓存重用。
const client = new ApolloClient({cache: new InMemoryCache({typePolicies: {Query: {fields: {feed: {keyArgs: false,merge(existing = [], incoming, { args }) {const offset = args?.offset ?? 0;const merged = existing ? existing.slice(0) : [];for (let i = 0; i < incoming.length; i++) {merged[offset + i] = incoming[i];}return merged;}}}}}})
});
10.2 实战场景:用户详情页的局部更新
在用户详情页中,结合 cache.modify 实现对个人信息的局部更新(如昵称、个人简介),避免整页重新查询,提升响应速度。
通过对 id 与 字段路径 的精确定位,可以确保变更只影响目标数据,不影响其他缓存条目。
cache.modify({id: cache.identify({ __typename: 'User', id: 'u-001' }),fields: {nickname() {return '新昵称';},about(existing) {return existing + ' 更新';}}
});


