Apollo 客户端的键参数
使用 keyArgs API
我们建议您先阅读核心分页 API,然后再学习特定于keyArgs
配置的注意事项。
configureApollo 客户端缓存可以为单个 schema 字段存储多个条目 field。
例如,考虑这个Query.user
字段:
type Query {# Returns whichever User object corresponds to `id`user(id: ID!): User}
如果我们查询具有以下id
的User
:1和2,Apollo Client 缓存将保存如下条目:
{'ROOT_QUERY': {'user({"id":"1"})': {'__ref': 'User:1'},'user({"id":"2"})': {'__ref': 'User:2'}}}
如图所示,每个条目的存储键包括相应的参数值。这意味着,如果查询中某个字段的不同参数的值不同,存储键也会不同,这些查询将产生不同的缓存条目。
如果一个字段没有参数,其存储键就是其名称。
这种默认行为是出于安全考虑:缓存不知道它是否能合并无参数组合返回的不同值而不会使数据无效。在上述示例中,缓存肯定不应该合并查询具有id
1和2
的用户User
.
分页问题
某些参数不应导致Apollo Client 缓存存储单独的条目。对于与分页列表相关的参数来说,这几乎总是情况。
考虑这个Query.feed
字段:
type Query {feed(offset: Int, limit: Int, category: Category): [FeedItem!]}
参数offset
和limit
使客户端可以指定它想要获取的“页面”。
# First queryquery GetFeedItems {feed(offset: 0, limit: 10, category: "SPORTS")}# Second queryquery GetFeedItems {feed(offset: 10, limit: 10, category: "SPORTS")}
但是,由于它们的参数值不同,这两个包含十个条目的列表默认会分别被缓存。这意味着当第二个查询完成时,返回的条目不会添加到原始列表中!
{'ROOT_QUERY': {// First query'feed({"offset":"0","limit":"10","category":"SPORTS"})': [{'__ref': 'FeedItem:1'},// ...additional items...],// Second query'feed({"offset":"10","limit":"10","category":"SPORTS"})': [{'__ref': 'FeedItem:11'},// ...additional items...]}}
在这种情况下,我们不希望将offset
或limit
包含在缓存条目的存储键中。相反,我们希望缓存将上述两个查询的结果合并为一个包含两个列表项目的单个缓存条目。
为了帮助处理这种情况,我们可以为字段设置键参数。
设置 keyArgs
一个 关键字参数是专为 GraphQL 字段定义的参数,这些参数包含在该字段的缓存存储键中。默认情况下,所有 GraphQL 参数都是关键字参数,如我们提供的消息来源示例所示:
{'ROOT_QUERY': {// First query'feed({"offset":"0","limit":"10","category":"SPORTS"})': [{'__ref': 'FeedItem:1'},// ...additional items...],// Second query'feed({"offset":"10","limit":"10","category":"SPORTS"})': [{'__ref': 'FeedItem:11'},// ...additional items...]}}
您可以通过定义特定字段的缓存 字段策略 来覆盖此默认行为:
const cache = new InMemoryCache({typePolicies: {Query: {fields: {feed: {keyArgs: ["category"],},},},},});
此字段策略针对 Query.feed
包括一个 keyArgs
数组,其中包含缓存应将其包含在存储键中的所有参数名称。
在这种情况下,我们不希望缓存将 offset
或 limit
作为关键字参数,因为这些参数不改变 我们 要异步获取的列表。但是,我们 需要 将 category
作为关键字参数,因为我们希望将我们的 SPORTS
源独立存储,与其他来源(如 FASHION
或 MUCl
)区别开来。
按上述方式设置 keyArgs
后,我们最终得到我们 SPORTS
源的单个缓存条目(注意存储键中缺少 offset
和 limit
):
{'ROOT_QUERY': {'feed({"category":"SPORTS"})': [{'__ref': 'FeedItem:1'},// ...additional items from first query...{'__ref': 'FeedItem:11'},// ...additional items from second query...]}}
重要: 定义用于分页列表字段(如 Query.feed
)的 keyArgs
后,您还需要定义字段的 合并函数。否则,第二个查询返回的列表将 覆盖 第一个列表而不是与其合并。
支持 keyArgs
的值
您可以为字段提供以下值作为 field 的 keyArgs
keyArgs
数组
以下 \"keyArgs\" 数组可以包含的值的类型。缓存的字段的存储键使用数组的所有参数、指令和变量值。
参数名称:
// Here, category and id are two arguments of the field["category", "id"]嵌套 参数名称用于具有子字段的输入类型:字段:
// Here, details is an input type argument// with subfields name and date["details", ["name", "date"] ]指令名称(用
@
表示),可以带有其中一个或多个参数:// Here, @units is a directive that can be applied// to the field, and it has a type argument["@units", ["type"] ]变量名称(用
$
表示):// Here, $userId is a variable that's provided to some// operations that include the field["$userId"]
keyArgs
函数(高级)
您可以通过向keyArgs
提供自定义函数来为字段存储键定义完全不同的格式。此函数接收字段的参数和其他上下文作为参数,并且可以返回任何字符串作为存储键(或动态生成keyArgs
数组)。
这仅用于高级用例。有关详细信息,请参阅FieldPolicy
API参考。
哪些参数属于 keyArgs
?
在决定哪些字段的参数应包含在keyArgs
中时,考虑两个极端:所有参数和没有参数可能有所帮助。这初始选项有助于演示添加或删除单个参数的影响。
使用所有参数
如果所有参数都是键参数(这是默认行为),则每个字段的参数值的不同组合都会产生不同的缓存条目。换句话说,更改任何参数值都会导致不同的存储键,因此返回的值会单独存储。我们在分页示例中看到这一点:
{'ROOT_QUERY': {// First query'feed({"offset":"0","limit":"10","category":"SPORTS"})': [{'__ref': 'FeedItem:1'},// ...additional items...],// Second query'feed({"offset":"10","limit":"10","category":"SPORTS"})': [{'__ref': 'FeedItem:11'},// ...additional items...]}}
采用此方法,Apollo 客户端无法为字段返回缓存的值,除非所有字段参数与之前缓存的某个结果匹配。这会显著降低缓存的命中率,但同时也防止了当参数差异相关(如我们的User
示例)时缓存返回错误值:
{'ROOT_QUERY': {'user({"id":"1"})': {'__ref': 'User:1'},'user({"id":"2"})': {'__ref': 'User:2'}}}
不使用参数
如果没有参数是键参数(您可以通过设置keyArgs: false
进行配置),则字段的存储键只是字段名称,没有任何参数值附加到它。这意味着默认情况下,每次查询为该字段返回值时,该值都会替换已存储的任何值。
此默认行为通常是不希望的(特别是对于分页列表),因此您可以定义使用参数值确定如何将新返回的值与现有的缓存值组合的read
和merge
函数。
示例
回忆一下在此处Query.feed
字段:
type Query {feed(offset: Int, limit: Int, category: Category): [FeedItem!]}
我们最初将 keyArgs: ["category"]
设置为这个字段,以保持不同类别的信息条目分离。通过设置 keyArgs: false
并定义以下 read
和 merge
函数,我们可以实现相同的功能:
const cache = new InMemoryCache({typePolicies: {Query: {fields: {feed: {keyArgs: false,read(existing = {}, { args: { offset, limit, category }}) {return existing[category]?.slice(offset, offset + limit);},merge(existing = {}, incoming, { args: { category, offset = 0 }}) {const merged = existing[category] ? existing[category].slice(0) : [];for (let i = 0; i < incoming.length; ++i) {merged[offset + i] = incoming[i];}existing[category] = merged;return existing;},},},},},});
上面的代码中,传递给我们的 read
和 merge
函数的 map 是类别名称与 FeedItem
列表之间的映射。这个映射使得单个缓存的字段值可以存储多个不同的列表。这种手动分离在逻辑上等效于使用 keyArgs: ["category"]
,因此额外的努力通常是不必要的。
如果我们知道不同 category
值的源具有不同的数据,并且我们知道我们的 read
函数不需要同时访问多个类别源,我们可以安全地将 category
参数的责任转移到 keyArgs
上。这将使我们能够简化我们的 read
和 merge
函数,使其一次只处理一个源。
总结
如果存储和检索给定 字段's 数据的逻辑对不同 参数 (如上例中的 category
) 的不同值是相同的,并且不同的 字段 值在逻辑上彼此独立,那么您可能应该在 keyArgs
中添加该 arg
以避免在 read
和 merge
函数中处理它。
相比之下,那些限制、过滤、排序或其他重新处理现有 字段 数据的 参数 通常不应该在 keyArgs
中。这是因为将它们放在 keyArgs
中会使存储密钥更加多样化,从而降低缓存命中率并限制您使用不同的 arg
获取相同数据的不同视图的能力。
一般来说, read
和 merge
函数可以使用您的缓存 字段 数据执行几乎所有操作,但 keyArgs
通常以更少的代码复杂性提供类似的功能。尽可能优先考虑 keyArgs
的限制性声明性 API,而不是 merge
和 read
等函数的无限能力。
连接指示符 @connection
顾名思义 @connection
指示符是 Apollo Client 支持的一种 Relay 风格约定。然而,我们建议使用 keyArgs
,因为您可以通过单个 keyArgs
配置实现相同的效果,而您需要将 @connection
指示符包含在每个发送到服务器的 query
中。
换句话说,虽然Relay鼓励以下@connection(...)
指示符用于Query.feed
查询:
const FEED_QUERY = gql`query Feed($category: FeedCategory!, $offset: Int, $limit: Int) {feed(category: $category, offset: $offset, limit: $limit) @connection(key: "feed",filter: ["category"]) {edges {node { ... }}pageInfo {endCursorhasNextPage}}}`;
在Apollo Client中,您可以使用以下查询(没有@connection(...)
指示符的相同查询):
const FEED_QUERY = gql`query Feed($category: FeedCategory!, $offset: Int, $limit: Int) {feed(category: $category, offset: $offset, limit: $limit) {edges {node { ... }}pageInfo {endCursorhasNextPage}}}`;
相反,您应该配置您的Query.feed
字段策略中的keyArgs
:
const cache = new InMemoryCache({typePolicies: {Query: {fields: {feed: {keyArgs: ["category"],},},},},})
如果Query.feed
字段没有category
之类的keyArgs: [...]
可以使用自变量,那么使用@connection
指示符也是有意义的:
const FEED_QUERY = gql`query Feed($offset: Int, $limit: Int, $feedKey: String) {feed(offset: $offset, limit: $limit) @connection(key: $feedKey) {edges {node { ... }}pageInfo {endCursorhasNextPage}}}`;
如果您使用不同的$feedKey
变量值执行此查询,这些feed
结果将分别存储在缓存中,而通常它们都会存储在同一个列表中。
在选择此Query.feed
字段keyArgs
配置时,应将@connection
指示符作为参数处理(@
告诉InMemoryCache
这是一个指示符):
const cache = new InMemoryCache({typePolicies: {Query: {fields: {feed: {keyArgs: ["@connection", ["key"]],},},},},})
使用此配置,您的缓存使用一个feed:{"@connection":{"key":...}}
键而不是简单的feed
来存储{ edges, pageInfo }
对象,这些对象位于ROOT_QUERY
对象内:
expect(cache.extract()).toEqual({ROOT_QUERY: {__typename: "Query",'feed:{"@connection":{"key":"some feed key"}}': { edges, pageInfo },'feed:{"@connection":{"key":"another feed key"}}': { edges, pageInfo },'feed:{"@connection":{"key":"yet another key"}}': { edges, pageInfo },// ...},})
在keyArgs: ["@connection", ["key"]]
中的["key"]
表示只考虑@connection
指示符的key
参数,并忽略任何其他参数(如过滤器)。将仅键传递给@connection
通常是足够的,但如果您还想传递一个filter: ["someArg", "anotherArg"]
参数,应将这些参数名称直接包含在keyArgs
中:
const cache = new InMemoryCache({typePolicies: {Query: {fields: {feed: {keyArgs: ["someArg", "anotherArg", "@connection", ["key"]],},},},},})
如果这些参数或指示符中的任何一个在当前查询中未提供,它们将自动从字段键中省略,而不会出错。这意味着通常在keyArgs
中包含更多参数或指示符比预期收到的要安全。
如上所述,如果keyArgs
数组不足以指定所需的字段键,您可以替代地传递一个函数作为keyArgs
参数,它接受args
对象和一个{ typename, field, fieldName, variables }
上下文参数。此函数可以返回一个字符串或一个动态生成的keyArgs
数组:
尽管keyArgs
(以及@connection
)对于除了分页字段之外的用途也很有用,但值得注意的是,relayStylePagination
默认情况下将keyArgs
配置为false
。您可以通过传递一个不同的值给relayStylePagination
来重新配置此keyArgs
行为:
const cache = new InMemoryCache({typePolicies: {Query: {fields: {feed: relayStylePagination(["type", "@connection", ["key"]]),},},},})
在不太可能的情况下,如果一个keyArgs
数组不足以捕获字段的身份,请记住,您可以为keyArgs
传递一个函数,这允许您以您希望的方式序列化args
对象。