You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Apollo Client分页API缓存命中仍发请求,如何自定义cache-first策略?

问题

我采用Apollo Client核心分页API方案,通过merge函数累积分页请求数据,计划用read函数重新分页。目前功能正常,但存在问题:即使目标页面的所有内容已在缓存中,每次切换页面仍会发起请求,违背了重新分页的初衷。

我使用默认的fetchStrategy——cache-first,官方定义为:如果缓存中存在所有请求的数据,则直接返回;否则执行服务器查询并缓存结果。

我疑惑的是:在分页实现场景下,Apollo Client如何检查缓存中是否存在所有请求数据?当前即使keyArgs匹配且数据已在缓存中,仍会发起请求。请问是什么原因导致该问题?如何自定义cache-first策略,使其检查请求页面的所有条目是否已存在于缓存中?

参考代码

TypePolicy配置

typePolicies: {
  Query: {
    fields: {
      paginatedProductTracking: {
        // Include everything except 'skip' and 'take' to be able to use `fetchMore`
        //  and repaginate when reading cache
        //  (essential for switching between desktop pagination and mobile lazy loading
        //   without having to refetch)
        keyArgs: (args) => JSON.stringify(omit(args, ['query.skip', 'query.take'])),
        merge: (existing, incoming, { args }) => {
          if (!existing) {
            return incoming;
          }
          if (!incoming) {
            return existing;
          }
          const data = existing.paginatedData;
          const newData = incoming.paginatedData;
          return {
            ...existing,
            // conservative merge that is robust against pages being requested out of order
            paginatedData: [
              ...data.slice(0, args?.query.skip || 0),
              ...newData,
              ...data.slice((args?.query.skip || 0) + newData.length),
            ],
          };
        },
      },
    },
  },
},

组件内查询代码

const [pageSize, setPageSize] = useState(100);
const [page, setPage] = useState(0);
const skip = page * pageSize;

const query = {
  filter,
  aggregationInterval,
  order,
  skip,
  take: pageSize,
  search: search ? values : null,
  locations: currentLocations.length > 0 ? currentLocations.map((location) => location.id) : undefined,
};

const { data, loading, fetchMore } = useProductTrackingAggregatedDataQuery({
  variables: {
    query,
  },
});

onPageChange={async (newPage) => {
  await fetchMore({
    variables: {
      query: {
        ...query,
        skip: newPage * pageSize,
      },
    },
  });
  setPage(newPage);
}}

原因分析

  • 缺少read函数:当前typePolicies仅配置了merge,未实现read函数。Apollo Client默认缓存读取逻辑无法识别合并后的分页数据结构,无法判断目标页面内容是否已在缓存中。
  • cache-first默认检查逻辑:默认情况下,cache-first会检查缓存中是否存在与当前查询参数完全匹配的完整结果。但由于你将skip和take排除在keyArgs之外,缓存中不会存储每个页面的单独完整结果,而是存储合并后的数据集。此时Apollo无法自动从合并数据中提取目标页面内容,只能发起新请求。

解决方法

1. 实现read函数,从缓存提取目标页面数据

在paginatedProductTracking的字段配置中添加read函数,根据当前请求的skip和take参数,从合并后的paginatedData中截取对应页面内容,并判断缓存是否包含目标页面的所有条目,以此决定是否触发请求。

修改后的TypePolicy配置:

typePolicies: {
  Query: {
    fields: {
      paginatedProductTracking: {
        keyArgs: (args) => JSON.stringify(omit(args, ['query.skip', 'query.take'])),
        merge: (existing, incoming, { args }) => {
          if (!existing) {
            return incoming;
          }
          if (!incoming) {
            return existing;
          }
          const data = existing.paginatedData;
          const newData = incoming.paginatedData;
          return {
            ...existing,
            paginatedData: [
              ...data.slice(0, args?.query.skip || 0),
              ...newData,
              ...data.slice((args?.query.skip || 0) + newData.length),
            ],
          };
        },
        read: (existing, { args }) => {
          if (!existing) return undefined; // 缓存无数据,触发请求

          const { skip = 0, take = 100 } = args?.query || {};
          const endIndex = skip + take;
          
          // 检查缓存数据是否覆盖目标页面所有条目
          if (existing.paginatedData.length >= endIndex) {
            // 从缓存截取目标页面数据
            return {
              ...existing,
              paginatedData: existing.paginatedData.slice(skip, endIndex),
            };
          }

          // 缓存缺少目标页面部分数据,返回undefined触发请求
          return undefined;
        },
      },
    },
  },
},

2. 调整页面切换逻辑

通过上述read函数的实现,cache-first策略会自动处理请求:如果缓存中已有完整页面数据,直接返回;否则才发起请求。无需在onPageChange中强制调用fetchMore,可直接更新页面状态,由查询自动读取缓存或发起请求。

关键说明

  • read函数是核心:负责将缓存中合并的完整数据集转换为当前查询请求的分页结果,同时通过判断数据长度是否覆盖目标页面范围,决定是否触发请求。
  • keyArgs配置保持不变:确保不同skip/take的请求共享同一个缓存条目,保证merge能正确累加数据。

内容的提问来源于stack exchange,提问作者Markus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 02:45:34