名称: apollo-caching-strategies 用户可调用: false 描述: 用于实现Apollo缓存策略,包括缓存策略、乐观UI、缓存更新和规范化。 允许的工具:
- 读取
- 写入
- 编辑
- Grep
- Glob
- Bash
Apollo缓存策略
掌握Apollo Client的缓存机制,用于构建具有优化数据获取和状态管理策略的高性能应用。
概述
Apollo Client的智能缓存是一个规范化的内存数据存储,允许高效的数据获取和更新。理解缓存策略和管理策略对于构建高性能应用至关重要。
安装和设置
缓存配置
// apollo/cache.js
import { InMemoryCache, makeVar } from '@apollo/client';
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
// 带有偏移量的分页
keyArgs: ['filter'],
merge(existing = [], incoming, { args }) {
const merged = existing.slice(0);
const offset = args?.offset || 0;
for (let i = 0; i < incoming.length; i++) {
merged[offset + i] = incoming[i];
}
return merged;
}
}
}
},
Post: {
keyFields: ['id'],
fields: {
comments: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
},
User: {
keyFields: ['email'],
fields: {
fullName: {
read(_, { readField }) {
return `${readField('firstName')} ${readField('lastName')}`;
}
}
}
}
}
});
核心模式
1. 获取策略
// 不同用例的不同获取策略
import { useQuery } from '@apollo/client';
import { GET_POSTS } from './queries';
// cache-first (默认): 首先检查缓存,如果未找到则使用网络
function CacheFirstPosts() {
const { data } = useQuery(GET_POSTS, {
fetchPolicy: 'cache-first'
});
return <PostsList posts={data?.posts} />;
}
// cache-only: 从不进行网络请求,缓存或错误
function CacheOnlyPosts() {
const { data } = useQuery(GET_POSTS, {
fetchPolicy: 'cache-only'
});
return <PostsList posts={data?.posts} />;
}
// cache-and-network: 立即返回缓存,用网络更新
function CacheAndNetworkPosts() {
const { data, loading, networkStatus } = useQuery(GET_POSTS, {
fetchPolicy: 'cache-and-network',
notifyOnNetworkStatusChange: true
});
return (
<div>
{networkStatus === 1 && <Spinner />}
<PostsList posts={data?.posts} />
</div>
);
}
// network-only: 总是进行网络请求,更新缓存
function NetworkOnlyPosts() {
const { data } = useQuery(GET_POSTS, {
fetchPolicy: 'network-only'
});
return <PostsList posts={data?.posts} />;
}
// no-cache: 总是进行网络请求,不更新缓存
function NoCachePosts() {
const { data } = useQuery(GET_POSTS, {
fetchPolicy: 'no-cache'
});
return <PostsList posts={data?.posts} />;
}
// standby: 类似于cache-first但不自动更新
function StandbyPosts() {
const { data, refetch } = useQuery(GET_POSTS, {
fetchPolicy: 'standby'
});
return (
<div>
<button onClick={() => refetch()}>刷新</button>
<PostsList posts={data?.posts} />
</div>
);
}
2. 缓存读取和写入
// apollo/cacheOperations.js
import { gql } from '@apollo/client';
// 从缓存读取
export function readPostFromCache(client, postId) {
try {
const data = client.readQuery({
query: gql`
query GetPost($id: ID!) {
post(id: $id) {
id
title
body
}
}
`,
variables: { id: postId }
});
return data?.post;
} catch (error) {
console.error('Post not in cache:', error);
return null;
}
}
// 写入缓存
export function writePostToCache(client, post) {
client.writeQuery({
query: gql`
query GetPost($id: ID!) {
post(id: $id) {
id
title
body
}
}
`,
variables: { id: post.id },
data: { post }
});
}
// 读取片段
export function readPostFragment(client, postId) {
return client.readFragment({
id: `Post:${postId}`,
fragment: gql`
fragment PostFields on Post {
id
title
body
likesCount
}
`
});
}
// 写入片段
export function updatePostLikes(client, postId, likesCount) {
client.writeFragment({
id: `Post:${postId}`,
fragment: gql`
fragment PostLikes on Post {
likesCount
}
`,
data: {
likesCount
}
});
}
// 修改缓存字段
export function incrementPostLikes(client, postId) {
client.cache.modify({
id: client.cache.identify({ __typename: 'Post', id: postId }),
fields: {
likesCount(currentCount = 0) {
return currentCount + 1;
},
isLiked() {
return true;
}
}
});
}
3. 乐观更新
// components/OptimisticLike.js
import { useMutation } from '@apollo/client';
import { LIKE_POST } from '../mutations';
function OptimisticLike({ post }) {
const [likePost] = useMutation(LIKE_POST, {
variables: { postId: post.id },
// 乐观响应
optimisticResponse: {
__typename: 'Mutation',
likePost: {
__typename: 'Post',
id: post.id,
likesCount: post.likesCount + 1,
isLiked: true
}
},
// 更新缓存
update(cache, { data: { likePost } }) {
cache.modify({
id: cache.identify(post),
fields: {
likesCount() {
return likePost.likesCount;
},
isLiked() {
return likePost.isLiked;
}
}
});
},
// 处理错误
onError(error) {
console.error('Like failed, reverting:', error);
// 乐观更新自动回退
}
});
return (
<button onClick={() => likePost()}>
{post.isLiked ? 'Unlike' : 'Like'} ({post.likesCount})
</button>
);
}
// 复杂乐观更新,涉及多个更改
function OptimisticCreateComment({ postId }) {
const [createComment] = useMutation(CREATE_COMMENT, {
optimisticResponse: ({ body }) => ({
__typename: 'Mutation',
createComment: {
__typename: 'Comment',
id: `temp-${Date.now()}`,
body,
createdAt: new Date().toISOString(),
author: {
__typename: 'User',
id: currentUser.id,
name: currentUser.name,
avatar: currentUser.avatar
}
}
}),
update(cache, { data: { createComment } }) {
// 添加评论到帖子
cache.modify({
id: cache.identify({ __typename: 'Post', id: postId }),
fields: {
comments(existing = []) {
const newCommentRef = cache.writeFragment({
data: createComment,
fragment: gql`
fragment NewComment on Comment {
id
body
createdAt
author {
id
name
avatar
}
}
`
});
return [...existing, newCommentRef];
},
commentsCount(count = 0) {
return count + 1;
}
}
});
}
});
return <CommentForm onSubmit={createComment} />;
}
4. 缓存驱逐
// apollo/eviction.js
export function evictPost(client, postId) {
// 驱逐特定帖子
client.cache.evict({
id: client.cache.identify({ __typename: 'Post', id: postId })
});
// 垃圾回收
client.cache.gc();
}
export function evictField(client, postId, fieldName) {
// 驱逐特定字段
client.cache.evict({
id: client.cache.identify({ __typename: 'Post', id: postId }),
fieldName
});
}
export function evictAllPosts(client) {
// 驱逐缓存中所有帖子
client.cache.modify({
fields: {
posts(existing, { DELETE }) {
return DELETE;
}
}
});
client.cache.gc();
}
// 在删除突变中使用
function DeletePost({ postId }) {
const [deletePost] = useMutation(DELETE_POST, {
variables: { id: postId },
update(cache) {
// 从帖子列表中移除
cache.modify({
fields: {
posts(existingPosts = [], { readField }) {
return existingPosts.filter(
ref => postId !== readField('id', ref)
);
}
}
});
// 驱逐帖子及相关数据
cache.evict({ id: cache.identify({ __typename: 'Post', id: postId }) });
cache.gc();
}
});
return <button onClick={() => deletePost()}>删除</button>;
}
5. 响应式变量
// apollo/reactiveVars.js
import { makeVar, useReactiveVar } from '@apollo/client';
// 创建响应式变量
export const cartItemsVar = makeVar([]);
export const themeVar = makeVar('light');
export const isModalOpenVar = makeVar(false);
export const notificationsVar = makeVar([]);
// 辅助函数
export function addToCart(item) {
const cart = cartItemsVar();
cartItemsVar([...cart, item]);
}
export function removeFromCart(itemId) {
const cart = cartItemsVar();
cartItemsVar(cart.filter(item => item.id !== itemId));
}
export function clearCart() {
cartItemsVar([]);
}
export function toggleTheme() {
const current = themeVar();
themeVar(current === 'light' ? 'dark' : 'light');
}
export function addNotification(notification) {
const notifications = notificationsVar();
notificationsVar([...notifications, {
id: Date.now(),
...notification
}]);
}
// React组件使用
function Cart() {
const cartItems = useReactiveVar(cartItemsVar);
return (
<div>
<h2>购物车 ({cartItems.length})</h2>
{cartItems.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => removeFromCart(item.id)}>移除</button>
</div>
))}
</div>
);
}
// 在缓存配置中使用
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
cartItems: {
read() {
return cartItemsVar();
}
},
theme: {
read() {
return themeVar();
}
}
}
}
}
});
6. 分页策略
// 基于偏移的分页
const POSTS_QUERY = gql`
query GetPosts($limit: Int!, $offset: Int!) {
posts(limit: $limit, offset: $offset) {
id
title
body
}
}
`;
function OffsetPagination() {
const { data, fetchMore } = useQuery(POSTS_QUERY, {
variables: { limit: 10, offset: 0 }
});
return (
<div>
<PostsList posts={data?.posts} />
<button
onClick={() =>
fetchMore({
variables: { offset: data.posts.length }
})
}
>
加载更多
</button>
</div>
);
}
// 基于游标的分页
const CURSOR_POSTS_QUERY = gql`
query GetPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
cursor
node {
id
title
body
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
function CursorPagination() {
const { data, fetchMore } = useQuery(CURSOR_POSTS_QUERY, {
variables: { first: 10 }
});
return (
<div>
{data?.posts.edges.map(({ node }) => (
<Post key={node.id} post={node} />
))}
{data?.posts.pageInfo.hasNextPage && (
<button
onClick={() =>
fetchMore({
variables: {
after: data.posts.pageInfo.endCursor
}
})
}
>
加载更多
</button>
)}
</div>
);
}
// 分页缓存配置
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
keyArgs: ['filter'],
merge(existing, incoming, { args }) {
if (!existing) return incoming;
const { offset = 0 } = args;
const merged = existing.slice(0);
for (let i = 0; i < incoming.length; i++) {
merged[offset + i] = incoming[i];
}
return merged;
}
}
}
}
}
});
// Relay风格分页,使用offsetLimitPagination
import { offsetLimitPagination } from '@apollo/client/utilities';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: offsetLimitPagination()
}
}
}
});
7. 缓存持久化
// apollo/persistedCache.js
import { InMemoryCache } from '@apollo/client';
import { persistCache, LocalStorageWrapper } from 'apollo3-cache-persist';
export async function createPersistedCache() {
const cache = new InMemoryCache({
typePolicies: {
// 您的类型策略
}
});
await persistCache({
cache,
storage: new LocalStorageWrapper(window.localStorage),
maxSize: 1048576, // 1 MB
debug: true,
trigger: 'write', // 或 'background'
});
return cache;
}
// 在客户端设置中使用
import { ApolloClient } from '@apollo/client';
async function initApollo() {
const cache = await createPersistedCache();
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache
});
return client;
}
// 清除持久化缓存
export function clearPersistedCache(client) {
client.clearStore(); // 清除缓存
localStorage.clear(); // 清除持久化
}
// 选择性持久化
const cache = new InMemoryCache({
typePolicies: {
User: {
fields: {
// 不持久化敏感数据
authToken: {
read() {
return null;
}
}
}
}
}
});
8. 缓存预热
// apollo/cacheWarming.js
import { gql } from '@apollo/client';
export async function warmCache(client) {
// 预加载关键查询
await Promise.all([
client.query({
query: gql`
query GetCurrentUser {
me {
id
name
email
}
}
`
}),
client.query({
query: gql`
query GetRecentPosts {
posts(limit: 20) {
id
title
excerpt
}
}
`
})
]);
}
// 悬停预取
function PostLink({ postId }) {
const client = useApolloClient();
const prefetch = () => {
client.query({
query: GET_POST,
variables: { id: postId }
});
};
return (
<Link
to={`/posts/${postId}`}
onMouseEnter={prefetch}
onTouchStart={prefetch}
>
查看帖子
</Link>
);
}
9. 缓存重定向
// apollo/cache.js
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
post: {
read(_, { args, toReference }) {
// 重定向到缓存对象
return toReference({
__typename: 'Post',
id: args.id
});
}
}
}
},
User: {
fields: {
// 从缓存计算字段
fullName: {
read(_, { readField }) {
const firstName = readField('firstName');
const lastName = readField('lastName');
return `${firstName} ${lastName}`;
}
},
// 带参数的字段
posts: {
read(existing, { args, readField }) {
if (args?.published !== undefined) {
return existing?.filter(ref =>
readField('published', ref) === args.published
);
}
return existing;
}
}
}
}
}
});
10. 缓存监控和调试
// apollo/monitoring.js
export function logCacheContents(client) {
const cache = client.extract();
console.log('缓存内容:', cache);
}
export function watchCacheChanges(client) {
const observer = client.cache.watch({
query: gql`
query GetAllData {
posts {
id
title
}
}
`,
callback: (data) => {
console.log('缓存变化:', data);
}
});
return observer;
}
// 开发助手
if (process.env.NODE_ENV === 'development') {
window.apolloClient = client;
window.logCache = () => logCacheContents(client);
// 缓存大小监控
setInterval(() => {
const cacheSize = JSON.stringify(client.extract()).length;
console.log(`缓存大小: ${(cacheSize / 1024).toFixed(2)} KB`);
}, 10000);
}
// React DevTools集成
import { ApolloClient } from '@apollo/client';
import { ApolloProvider } from '@apollo/client/react';
function App() {
return (
<ApolloProvider client={client}>
{/* 启用Apollo DevTools */}
<YourApp />
</ApolloProvider>
);
}
// 自定义缓存检查器
function CacheInspector() {
const client = useApolloClient();
const [cacheData, setCacheData] = useState({});
useEffect(() => {
const data = client.extract();
setCacheData(data);
}, [client]);
return (
<div>
<h2>缓存检查器</h2>
<pre>{JSON.stringify(cacheData, null, 2)}</pre>
<button onClick={() => client.clearStore()}>清除缓存</button>
</div>
);
}
最佳实践
- 选择适当的获取策略 - 根据数据新鲜度需求匹配策略
- 使用乐观更新 - 提升感知性能
- 正确规范化缓存 - 正确配置keyFields
- 实现分页 - 高效处理大数据集
- 持久化关键数据 - 缓存认证状态和用户偏好
- 监控缓存大小 - 防止内存膨胀
- 使用响应式变量 - 高效管理本地状态
- 策略性地预热缓存 - 预取关键数据
- 驱逐未使用数据 - 清理删除后的数据
- 调试缓存问题 - 有效使用Apollo DevTools
常见陷阱
- 错误的获取策略 - 对实时数据使用cache-first
- 缓存非规范化 - 缺少或不正确的keyFields
- 内存泄漏 - 未驱逐删除的项
- 过度缓存 - 缓存过多数据
- 陈旧数据 - 未正确失效缓存
- 缺少更新 - 突变后忘记更新缓存
- 错误的合并逻辑 - 错误的分页合并逻辑
- 缓存颠簸 - 过多的缓存写入
- 持久化问题 - 存储敏感数据
- 无错误处理 - 未处理缓存读取失败
使用场景
- 构建数据密集型应用
- 实现离线优先功能
- 创建实时协作应用
- 开发移动应用
- 构建电子商务平台
- 创建社交媒体应用
- 实现复杂状态管理
- 开发管理仪表板
- 构建内容管理系统
- 创建分析应用