参加 10 月 8-10 日在纽约市的 GraphQL Federation 和 API 平台工程最新技巧、趋势和新闻的讨论。参加 2024 年纽约市的 GraphQL Summit
文档
免费开始

Apollo 客户端的键参数

使用 keyArgs API


我们建议您先阅读核心分页 API,然后再学习特定于keyArgs配置的注意事项。

configure缓存可以为单个 schema 字段存储多个条目

例如,考虑这个Query.user 字段:

type Query {
# Returns whichever User object corresponds to `id`
user(id: ID!): User
}

如果我们具有以下idUser12,Apollo Client 缓存将保存如下条目:

缓存
{
'ROOT_QUERY': {
'user({"id":"1"})': {
'__ref': 'User:1'
},
'user({"id":"2"})': {
'__ref': 'User:2'
}
}
}

如图所示,每个条目的存储键包括相应的值。这意味着,如果查询中某个字段的不同参数的值不同,存储键也会不同,这些查询将产生不同的缓存条目。

如果一个字段没有参数,其存储键就是其名称。

这种默认行为是出于安全考虑:缓存不知道它是否能合并无参数组合返回的不同值而不会使数据无效。在上述示例中,缓存肯定不应该合并查询具有id1和2的用户User.

分页问题

某些参数不应导致Apollo Client 缓存存储单独的条目。对于与分页列表相关的参数来说,这几乎总是情况。

考虑这个Query.feed字段:

type Query {
feed(offset: Int, limit: Int, category: Category): [FeedItem!]
}

参数offsetlimit 使客户端可以指定它想要获取的“页面”。

# First query
query GetFeedItems {
feed(offset: 0, limit: 10, category: "SPORTS")
}
# Second query
query 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...
]
}
}

在这种情况下,我们不希望将offsetlimit包含在缓存条目的存储键中。相反,我们希望缓存将上述两个查询的结果合并为一个包含两个列表项目的单个缓存条目。

为了帮助处理这种情况,我们可以为字段设置键参数

设置 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 数组,其中包含缓存应将其包含在存储键中的所有参数名称。

在这种情况下,我们不希望缓存将 offsetlimit 作为关键字参数,因为这些参数不改变 我们 要异步获取的列表。但是,我们 需要category 作为关键字参数,因为我们希望将我们的 SPORTS 源独立存储,与其他来源(如 FASHIONMUCl)区别开来。

按上述方式设置 keyArgs 后,我们最终得到我们 SPORTS 源的单个缓存条目(注意存储键中缺少 offsetlimit):

{
'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 的值

您可以为字段提供以下值作为 fieldkeyArgs

  • false(表示该字段没有关键字参数)
  • 一个数组,包含参数、指令和变量名称
  • 一个函数(高级功能)

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进行配置),则字段的存储键只是字段名称,没有任何参数值附加到它。这意味着默认情况下,每次查询为该字段返回值时,该值都会替换已存储的任何值。

此默认行为通常是不希望的(特别是对于分页列表),因此您可以定义使用参数值确定如何将新返回的值与现有的缓存值组合的readmerge函数。

示例

回忆一下在此处Query.feed字段

type Query {
feed(offset: Int, limit: Int, category: Category): [FeedItem!]
}

我们最初将 keyArgs: ["category"] 设置为这个字段,以保持不同类别的信息条目分离。通过设置 keyArgs: false 并定义以下 readmerge 函数,我们可以实现相同的功能:

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;
},
},
},
},
},
});

上面的代码中,传递给我们的 readmerge 函数的 map 是类别名称与 FeedItem 列表之间的映射。这个映射使得单个缓存的字段值可以存储多个不同的列表。这种手动分离在逻辑上等效于使用 keyArgs: ["category"],因此额外的努力通常是不必要的。

如果我们知道不同 category 值的源具有不同的数据,并且我们知道我们的 read 函数不需要同时访问多个类别源,我们可以安全地将 category 参数的责任转移到 keyArgs 上。这将使我们能够简化我们的 readmerge 函数,使其一次只处理一个源。

总结

如果存储和检索给定 字段's 数据的逻辑对不同 参数 (如上例中的 category) 的不同值是相同的,并且不同的 字段 值在逻辑上彼此独立,那么您可能应该在 keyArgs 中添加该 arg 以避免在 readmerge 函数中处理它。

相比之下,那些限制、过滤、排序或其他重新处理现有 字段 数据的 参数 通常不应该在 keyArgs 中。这是因为将它们放在 keyArgs 中会使存储密钥更加多样化,从而降低缓存命中率并限制您使用不同的 arg 获取相同数据的不同视图的能力。

一般来说, readmerge 函数可以使用您的缓存 字段 数据执行几乎所有操作,但 keyArgs 通常以更少的代码复杂性提供类似的功能。尽可能优先考虑 keyArgs 的限制性声明性 API,而不是 mergeread 等函数的无限能力。

连接指示符 @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 {
endCursor
hasNextPage
}
}
}
`;

在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 {
endCursor
hasNextPage
}
}
}
`;

相反,您应该配置您的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 {
endCursor
hasNextPage
}
}
}
`;

如果您使用不同的$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对象。

上一页
基于游标的分页
下一页
概述
评估文章评价

2024Apollo Graph Inc.,贸易名称为Apollo GraphQL。

隐私政策

公司