Apollo Client 的 InMemoryCache 使用的是一种 层次化的缓存策略,结合了:
- 基于
field + arguments
的关键词缓存(Query 层级) - 基于
__typename + id
的实体归一化缓存(Entity normalization)
这是 Apollo Cache 高性能和强可拓展性的根本所在。
📦 Apollo 缓存体系 详解
✅ 第一层:Query 层作为入口 ------ fieldName + arguments
唯一标识
当你发起一个 GraphQL 查询:
graphql
query GetBooks($category: String!) {
books(category: $category) {
id
title
}
}
假如你传了变量 { category: "fiction" }
,Apollo 会在缓存中以以下格式为入口:
jsonc
ROOT_QUERY: {
"books({\"category\":\"fiction\"})": [ // 对应的是 books 字段,带参数
{ __ref: "Book:1" },
{ __ref: "Book:2" }
]
}
这里的 key 是 books({"category":"fiction"})
,它是由字段名 + 参数组成。
✅ 第二层:实体归一化(Normalization) ------ __typename:id
为缓存主键
当 Apollo 发现你返回的数据中包含了一个对象,并且这个对象中有 __typename
+ id
(或者在 typePolicies
中配置的 key 字段),它会将其单独提取出来,作为唯一实体存储。
jsonc
"Book:1": {
id: "1",
__typename: "Book",
title: "The Great Gatsby"
},
"Book:2": {
id: "2",
__typename: "Book",
title: "1984"
}
这样做的好处:
- 去重:一本书可能出现在多个查询里,统一存储避免数据重复。
- 响应式更新 :当某个字段更新时(如
title
),所有引用它的地方都会自动响应式更新。 - 强大的缓存合并能力 :通过定制
typePolicies.merge()
,你可以合并数据,而不是覆盖。
🛠️ 自定义 Primary Key: typePolicies.keyFields
默认情况下,Apollo 使用 __typename
+ id
作为唯一主键,但你可以自定义:
ts
const cache = new InMemoryCache({
typePolicies: {
Book: {
keyFields: ["isbn"] // 用 isbn 替代 id
},
},
});
🤯 多层连接:如何看缓存结构?
查询嵌套字段时,如:
graphql
{
user {
id
name
posts {
id
title
}
}
}
缓存结构如下:
jsonc
ROOT_QUERY: {
"user": { __ref: "User:1" }
},
"User:1": {
id: "1",
__typename: "User",
name: "Alice",
posts: [
{ __ref: "Post:1" },
{ __ref: "Post:2" }
]
},
"Post:1": {
id: "1",
__typename: "Post",
title: "Hello World"
},
"Post:2": {
id: "2",
__typename: "Post",
title: "GraphQL Rocks"
}
🧠 如果内层对象没有提供id,缓存结构又是什么样了?
bash
graphql
{
user {
id
name
posts {
title
}
}
}
若只有 title
没有 id
/__typename
,Apollo cache 结构将类似:
bash
jsonc
{
"User:1": {
id: "1",
__typename: "User",
name: "Alice",
posts: [
{
title: "Hello World"
},
{
title: "GraphQL Rocks"
}
]
}
}
- ✅
posts
数组被内联嵌套存储 在User:1
的字段中。 - ❌ 而不是像
posts: [{ __ref: "Post:1" }]
这样使用__ref
。 - ❌ 这些 post 项不会被 Apollo 单独归一化处理。
🎯 总结
特性 | 描述 |
---|---|
查询记录 | Apollo 会将查询结果按 fieldName + arguments 存成一个入口(如 books({ category: "fiction" }) ) |
实体归一化 | 所有实体根据 __typename + id 作为主键,单独提取并保存 |
引用机制 | Query 层只是一个数组或对象的引用(__ref ) |
自定义主键 | 使用 keyFields 修改默认的主键策略 |
优势 | 响应式 UI 更新、缓存去重、高可定制性 |
💡 正因为如此,Apollo 的缓存才如此强大灵活。你可以根据查询字段缓存数据,也可以通过 id
快速访问或修改实体,同时还能避免冗余和状态不一致的问题。