Apollo缓存策略Skill apollo-caching-strategies

Apollo缓存策略是用于Apollo Client的缓存管理技术,包括缓存策略配置、乐观UI更新、缓存持久化和规范化等核心功能。它帮助开发者构建高性能的GraphQL应用,优化数据获取、状态管理和用户体验,适用于数据密集型前端开发。关键词:Apollo缓存、GraphQL、前端性能、状态管理、乐观更新。

前端开发 0 次安装 0 次浏览 更新于 3/25/2026

名称: 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>
  );
}

最佳实践

  1. 选择适当的获取策略 - 根据数据新鲜度需求匹配策略
  2. 使用乐观更新 - 提升感知性能
  3. 正确规范化缓存 - 正确配置keyFields
  4. 实现分页 - 高效处理大数据集
  5. 持久化关键数据 - 缓存认证状态和用户偏好
  6. 监控缓存大小 - 防止内存膨胀
  7. 使用响应式变量 - 高效管理本地状态
  8. 策略性地预热缓存 - 预取关键数据
  9. 驱逐未使用数据 - 清理删除后的数据
  10. 调试缓存问题 - 有效使用Apollo DevTools

常见陷阱

  1. 错误的获取策略 - 对实时数据使用cache-first
  2. 缓存非规范化 - 缺少或不正确的keyFields
  3. 内存泄漏 - 未驱逐删除的项
  4. 过度缓存 - 缓存过多数据
  5. 陈旧数据 - 未正确失效缓存
  6. 缺少更新 - 突变后忘记更新缓存
  7. 错误的合并逻辑 - 错误的分页合并逻辑
  8. 缓存颠簸 - 过多的缓存写入
  9. 持久化问题 - 存储敏感数据
  10. 无错误处理 - 未处理缓存读取失败

使用场景

  • 构建数据密集型应用
  • 实现离线优先功能
  • 创建实时协作应用
  • 开发移动应用
  • 构建电子商务平台
  • 创建社交媒体应用
  • 实现复杂状态管理
  • 开发管理仪表板
  • 构建内容管理系统
  • 创建分析应用

资源