加入我们,从10月8日至10日,在新 York City 学习关于 GraphQL 联邦和 API 平台工程的最新技巧、趋势和新闻。加入我们,一起参加2024年纽约市的 GraphQL 研讨会
文档
免费开始

核心分页 API

获取和缓存分页结果


无论你的分页策略是什么使用特定列表时,你的应用需要执行以下操作才能有效地查询该字段:

本文描述了分页字段的核心要求

fetchMore 函数

总是通过向 GraphQL 服务器发送后续查询来获取更多页面结果。在 Apollo Client 中,推荐使用fetchMore函数发送这些后续查询。此函数是ObservableQuery对象的一个成员,该对象是由client.watchQuery生成的,同时useQuery钩子也提供了此函数:

FeedWithData.jsx
const FEED_QUERY = gql`
query Feed($offset: Int, $limit: Int) {
feed(offset: $offset, limit: $limit) {
id
# ...
}
}
`;
const FeedWithData() {
const { loading, data, fetchMore } = useQuery(FEED_QUERY, {
variables: {
offset: 0,
limit: 10
},
});
// ...continues below...
}

您通常会在响应用户操作时调用fetchMore,例如点击按钮或滚动到无限滚动的源“底部”。

默认情况下,fetchMore将执行与原始查询形状和完全相同的查询。您可以为查询的变量(例如提供新的offset)传递新值,如下所示:

FeedWithData.jsx
const FeedWithData() {
// ...continuing from above...
// If you want your component to rerender with loading:true whenever
// fetchMore is called, add `notifyOnNetworkStatusChange:true` to the
// options you pass to useQuery.
if (loading) return 'Loading...';
return (
<Feed
entries={data.feed || []}
onLoadMore={() => fetchMore({
variables: {
offset: data.feed.length
},
})}
/>
);
}

在这里,我们将offset变量设置为feed.length以获取缓存列表中最后一个项目之后的条目。我们提供的这些变量将与为原始查询提供的变量合并,这意味着省略的变量(例如limit)在后续查询中保留其原始值(10)。

除变量外,您还可以选择性地提供执行查询的不同形状。这在内fetchMore需要获取单个分页字段时非常有用,但原始查询包含不相关的字段。

有关使用fetchMore的更多示例,请参阅基于偏移的分页基于光标的分页的详细文档。

我们的fetchMore函数已准备好,但我们还没有完成! 缓存尚不知道应当将后续查询的结果与原始查询的结果合并。相反,它将两个结果作为两个完全不同的列表存储。为了解决此问题,让我们继续学习合并分页结果

合并分页结果

本节中的示例使用基于偏移的分页,但本文适用于所有分页策略。

如上所述,一个 fetchMore 后续查询不会自动将其结果与原始查询的缓存结果合并。要实现这种行为,我们需要为我们的分页字段定义一个 字段策略

为什么需要字段策略?

假设我们有一个在 字段 中包含 中取值的

type Query {
user(id: ID!): User
}

现在,让我们执行以下 查询 两次,并为每次提供不同的 $id 变量值:

query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}

我们这两个查询返回了完全不同的 User 对象。有帮助的是,Apollo Client 缓存自动将这些两个对象分别存储,因为它看到至少有一个 字段 参数(id)提供了不同的值。否则,缓存可能会覆盖第一个 User 对象,并写入第二个 User 对象,但我们希望缓存两个对象!

现在,让我们执行 这个 查询两次,并分别提供不同的 $offset 变量值:

query Feed($offset: Int, $limit: Int) {
feed(offset: $offset, limit: $limit) {
id
# ...
}
}

在这种情况下,我们必须对分页列表字段进行两次 ,以获取两个不同的结果页面,我们希望这两页数据 合并。但是,缓存并不知道这一点!它没有看到这种情形和上面 User 场景之间的区别,因此它将结果存储为两个完全不同的列表。

通过字段策略,我们可以根据需要修改缓存的特定字段的行为。例如,我们可以告诉缓存 要根据 offsetlimit 的值独立存储 feed 字段的结果。下面是如何实现。

定义字段策略

字段策略指定了您的 InMemoryCache 中某个字段的读取和写入方式。您可以为字段定义一个策略,以合并分页查询的结果到一个单独的列表中。

示例

以下是我们消息源应用程序的服务器端模式,该应用程序使用基于偏移的分页:

type Query {
feed(offset: Int, limit: Int): [FeedItem!]
}
type FeedItem {
id: String!
message: String!
}

在我们的客户端,我们想要为 Query.feed 定义一个字段策略,使得所有返回的列表页面都被合并到我们的缓存中的单个列表中。

我们定义的字段策略位于提供给 InMemoryCache 构造函数的 typePolicies 选项中:

const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
feed: {
// Don't cache separate results based on
// any of this field's arguments.
keyArgs: false,
// Concatenate the incoming list items with
// the existing list items.
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
}
}
}
}
})

此字段策略指定了字段的关键参数以及一个 merge 函数。这两个配置对于处理分页都是必要的:

  • keyArgs 指定了字段的哪些参数会导致缓存为每个唯一参数组合保存一个不同的值。
    • 在我们的示例中,缓存不应该根据任何参数值(偏移量或限制)保存不同的结果。因此,我们通过传递 false 完全禁用这种行为。一个空数组(keyArgs: [])也可以工作,但 keyArgs: false 更加表达清晰,并且它使缓存中的字段键(在本例中是 feed)更加干净。
    • 如果一个特定参数的值可能导致返回完全不同的列表中的项目,那么该参数 应该 包含在 keyArgs 中。
    • 有关更多信息,请参阅 指定关键参数keyArgs API
  • 一个 merge 函数向 Apollo 客户端缓存指示如何将特定字段的传入数据与现有缓存数据相结合。如果没有这个函数,默认情况下,传入的字段值会覆盖现有的字段值。

使用此字段策略,缓存会自动合并以下结构的所有查询的结果,无论参数值如何:

// Client-side query definition
const FEED_QUERY = gql`
query Feed($offset: Int, $limit: Int) {
feed(offset: $offset, limit: $limit) {
id
message
}
}
`;

提升 merge 函数

上面的示例中,我们的 merge 函数是一行的:

merge(existing = [], incoming) {
return [...existing, ...incoming];
}

这个函数对客户端请求页面的顺序做出了危险假设,因为它忽略了 offsetlimit 的值。一个更健壮的 merge 函数可以使用 options.args 来决定将传入的数据相对于现有数据放在哪里,例如:

const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
feed: {
keyArgs: false,
merge(existing, incoming, { args: { offset = 0 }}) {
// Slicing is necessary because the existing data is
// immutable, and frozen in development.
const merged = existing ? existing.slice(0) : [];
for (let i = 0; i < incoming.length; ++i) {
merged[offset + i] = incoming[i];
}
return merged;
},
},
},
},
},
});

此逻辑以与单行策略相同的方式处理顺序页面写入,但它还可以容忍重复的、重叠的或顺序错误的写入,而不会重复任何列表项。

使用获取更多结果更新查询

有时需要对 fetchMore 的调用执行查询的附加缓存更新。虽然你可以使用 cache.readQuerycache.writeQuery 函数来自动执行此工作,但使用这两个函数可能很繁琐。

作为一个快捷方式,你可以向 fetchMore 提供 updateQuery 选项,以使用 fetchMore 调用的结果来更新查询。

注意

updateQuery 并不是你的字段策略 merge 函数的替代品。虽然你可以在不定义 merge 函数的情况下使用 updateQuery,但是对于查询中的字段定义的 merge 函数将使用 updateQuery 的结果运行。

让我们用 updateQuery 来合并结果而不是使用字段策略合并函数看看上面的例子:

fetchMore({
variables: { offset: data.feed.length },
updateQuery(previousData, { fetchMoreResult, variables: { offset }}) {
// Slicing is necessary because the existing data is
// immutable, and frozen in development.
const updatedFeed = previousData.feed.slice(0);
for (let i = 0; i < fetchMoreResult.feed.length; ++i) {
updatedFeed[offset + i] = fetchMoreResult.feed[i];
}
return { ...previousData, feed: updatedFeed };
},
})

💡 提示

我们建议定义包含至少一个 keyArgs值的字段策略,即使在您使用 updateQuery时也是如此。这可以防止在缓存中不必要地对数据进行碎片化。将 keyArgs设置为 false对于大多数情况来说足够了,这可以通过忽略 offsetlimit这两个参数,并将分页数据写为一个大数组来实现。

read 分页字段的函数

如上所示,一个 merge函数可以帮助您将您的 GraphQL 服务器返回的分区查询结果合并到你客户端缓存的单个列表中。但如果您也想配置如何读取本地缓存的该列表呢?为此,您可以定义一个 read函数

您在 字段策略中为字段定义一个 read函数,与 merge函数和 keyArgs一起。如果您为字段定义了 read函数,当您查询字段时,缓存将调用该函数,并将字段的现有缓存值(如果有的话)作为第一个参数传入。在查询响应中,字段会填充为 read函数的返回值,而不是现有的缓存值。

如果一个字段策略同时包含一个 merge函数和一个 read函数,那么 keyArgs的默认值变为 false(即没有参数是键参数)。如果任一函数都没有定义,则默认考虑该字段的所有参数为键参数。在任何情况下,您都可以定义 keyArgs来自定义默认行为。

分页字段的 read函数通常采用以下方法之一:

  • 重新分页,根据字段参数将缓存的列表分割成页。
  • 分页,始终返回完整的缓存列表。

虽然“正确”的方法因字段而异,但在无限滚动列表中,无分页的 code class="css-1lvdtfu">read函数通常效果最好,因为它可以让您的代码完全控制在给定时间内显示哪些元素,而不需要任何额外的缓存读取。

分页的 read 函数

列表字段的 read函数可以为该列表执行客户端重分页。它甚至可以在返回之前转换页面,例如按元素排序或过滤。

此功能不仅限于返回从您的服务器获取的相同页面,因为用于 offset/limit 分页的 read 函数可以从任何可用的 offset,以任何期望的 limit,读取:

const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
feed: {
read(existing, { args: { offset, limit }}) {
// A read function should always return undefined if existing is
// undefined. Returning undefined signals that the field is
// missing from the cache, which instructs Apollo Client to
// fetch its value from your GraphQL server.
return existing && existing.slice(offset, offset + limit);
},
// The keyArgs list and merge function are the same as above.
keyArgs: [],
merge(existing, incoming, { args: { offset = 0 }}) {
const merged = existing ? existing.slice(0) : [];
for (let i = 0; i < incoming.length; ++i) {
merged[offset + i] = incoming[i];
}
return merged;
},
},
},
},
},
});

根据您感到舒适做出的假设,您可能希望使此代码更加健壮。例如,您可以提供 offsetlimit 的默认值,以防有人调用 Query.feed 而不提供参数:

const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
feed: {
read(existing, {
args: {
// Default to returning the entire cached list,
// if offset and limit are not provided.
offset = 0,
limit = existing?.length,
} = {},
}) {
return existing && existing.slice(offset, offset + limit);
},
// ... keyArgs, merge ...
},
},
},
},
});

这种 read 函数样式负责根据字段参数重新分页您的数据,实际上反转了您的 merge 函数的行为。这样,您的应用程序可以使用不同的参数查询不同的页面。

非分页的 read 函数

对于分页字段的 read 函数可以选择 忽略 参数,如 offsetlimit,并且总是返回缓存在中的整个列表。在这种情况下,您的应用程序代码负责根据需要将该列表分成页面。

如果采用这种方法,可能不需要定义任何 read 函数,因为可以不进行任何处理即可返回缓存的列表。这就是为什么 offsetLimitPagination 辅助函数没有 read 函数实现的。

无论您如何配置 keyArgs,您的 read(以及 merge)函数都可以始终使用 options.args 对象检查传递给字段的任何参数。请参阅 keyArgs API》以更深入地讨论如何在 keyArgs 和您的 readmerge 函数之间分配参数处理责任。

使用 fetchMore 与设置了 no-cache 取消缓存策略的查询

注意

我们建议升级到版本 3.11.3 或更高版本,以解决在使用设置了 no-cache 取消缓存策略的查询时使用 fetchMore出现意外行为的问题。有关更多信息,请参阅拉取请求 #11974

上面显示的示例使用字段策略和合并函数来更新分页字段的结果。但是,对于使用no-cache提取策略的查询怎么办?数据不会写入缓存,因此字段策略没有作用。

为了更新我们的查询,我们向fetchMore函数提供了updateQuery选项。

让我们使用上面的例子,但是提供updateQuery函数给fetchMore来更新查询。

fetchMore({
variables: { offset: data.feed.length },
updateQuery(previousData, { fetchMoreResult, variables: { offset }}) {
// Slicing is necessary because the existing data is
// immutable, and frozen in development.
const updatedFeed = previousData.feed.slice(0);
for (let i = 0; i < fetchMoreResult.feed.length; ++i) {
updatedFeed[offset + i] = fetchMoreResult.feed[i];
}
return { ...previousData, feed: updatedFeed };
},
})

注意

截至Apollo Client版本3.11.3,当使用带有no-cache提取策略的fetchMore时,updateQuery选项是必需的。这是必要的,以便正确确定如何合并结果,因为在此过程中字段策略的merge函数被忽略。没有updateQuery函数调用fetchMore函数将会抛出错误。

上一页
概览
下一页
基于偏移量
评分文章评分在GitHub上编辑编辑论坛Discord

©2024Apache Graph Inc. 经由 Apollo GraphQL 进行商业运营。

隐私政策

公司