核心分页 API
获取和缓存分页结果
无论你的分页策略是什么GraphQL 服务器使用特定列表字段时,你的Apollo Client应用需要执行以下操作才能有效地查询该字段:
本文描述了分页字段的核心要求字段。
fetchMore
函数
总是通过向 GraphQL 服务器发送后续查询来获取更多页面结果。在 Apollo Client 中,推荐使用fetchMore
函数发送这些后续查询。此函数是ObservableQuery
对象的一个成员,该对象是由client.watchQuery
生成的,同时useQuery
钩子也提供了此函数:
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
)传递新值,如下所示:
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 (<Feedentries={data.feed || []}onLoadMore={() => fetchMore({variables: {offset: data.feed.length},})}/>);}
在这里,我们将offset
变量设置为feed.length
以获取缓存列表中最后一个项目之后的条目。我们提供的这些变量将与为原始查询提供的变量合并,这意味着省略的变量(例如limit
)在后续查询中保留其原始值(10
)。
除变量外,您还可以选择性地提供执行查询的不同形状。这在内fetchMore
需要获取单个分页字段时非常有用,但原始查询包含不相关的字段。
我们的fetchMore
函数已准备好,但我们还没有完成! 缓存尚不知道应当将后续查询的结果与原始查询的结果合并。相反,它将两个结果作为两个完全不同的列表存储。为了解决此问题,让我们继续学习合并分页结果。
合并分页结果
本节中的示例使用基于偏移的分页,但本文适用于所有分页策略。
如上所述,一个 fetchMore
后续查询不会自动将其结果与原始查询的缓存结果合并。要实现这种行为,我们需要为我们的分页字段定义一个 字段策略。
为什么需要字段策略?
假设我们有一个在 字段 中包含 GraphQL 记法中取值的 参数:
type Query {user(id: ID!): User}
现在,让我们执行以下 查询 两次,并为每次提供不同的 $id
变量值:
query GetUser($id: ID!) {user(id: $id) {idname}}
我们这两个查询返回了完全不同的 User
对象。有帮助的是,Apollo Client 缓存自动将这些两个对象分别存储,因为它看到至少有一个 字段 参数(id
)提供了不同的值。否则,缓存可能会覆盖第一个 User 对象,并写入第二个 User 对象,但我们希望缓存两个对象!
现在,让我们执行 这个 查询两次,并分别提供不同的 $offset
变量值:
query Feed($offset: Int, $limit: Int) {feed(offset: $offset, limit: $limit) {id# ...}}
在这种情况下,我们必须对分页列表字段进行两次 查询,以获取两个不同的结果页面,我们希望这两页数据 合并。但是,缓存并不知道这一点!它没有看到这种情形和上面 User
场景之间的区别,因此它将结果存储为两个完全不同的列表。
通过字段策略,我们可以根据需要修改缓存的特定字段的行为。例如,我们可以告诉缓存 不 要根据 offset
和 limit
的值独立存储 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 客户端缓存指示如何将特定字段的传入数据与现有缓存数据相结合。如果没有这个函数,默认情况下,传入的字段值会覆盖现有的字段值。- 有关更多信息,请参阅
merge
函数。
- 有关更多信息,请参阅
使用此字段策略,缓存会自动合并以下结构的所有查询的结果,无论参数值如何:
// Client-side query definitionconst FEED_QUERY = gql`query Feed($offset: Int, $limit: Int) {feed(offset: $offset, limit: $limit) {idmessage}}`;
提升 merge
函数
在 上面的示例中,我们的 merge
函数是一行的:
merge(existing = [], incoming) {return [...existing, ...incoming];}
这个函数对客户端请求页面的顺序做出了危险假设,因为它忽略了 offset
和 limit
的值。一个更健壮的 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.readQuery
和 cache.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
对于大多数情况来说足够了,这可以通过忽略 offset
和 limit
这两个参数,并将分页数据写为一个大数组来实现。
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;},},},},},});
根据您感到舒适做出的假设,您可能希望使此代码更加健壮。例如,您可以提供 offset
和 limit
的默认值,以防有人调用 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
函数可以选择 忽略 参数,如 offset
和 limit
,并且总是返回缓存在中的整个列表。在这种情况下,您的应用程序代码负责根据需要将该列表分成页面。
如果采用这种方法,可能不需要定义任何 read
函数,因为可以不进行任何处理即可返回缓存的列表。这就是为什么 offsetLimitPagination
辅助函数是 没有 read
函数实现的。
无论您如何配置 keyArgs
,您的 read
(以及 merge
)函数都可以始终使用 options.args
对象检查传递给字段的任何参数。请参阅 《keyArgs
API》以更深入地讨论如何在 keyArgs
和您的 read
或 merge
函数之间分配参数处理责任。
使用 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
函数将会抛出错误。