使用 GraphQL 缓存

Avatar of Jamie Barton
Jamie Barton

DigitalOcean 为您的旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

如果您最近开始使用 GraphQL,或者审查了它的优缺点,您无疑听说过诸如“GraphQL 不支持缓存”或“GraphQL 不关心缓存”之类的话。 对于大多数人来说,这是一个大问题。

官方的 GraphQL 文档 提到了缓存技术,因此,很明显,它背后的团队确实关心缓存及其性能优势。

关于 GraphQL 与缓存不兼容的看法是我想在这篇文章中解决的问题。 我想逐步介绍不同的缓存技术以及如何利用缓存来处理 GraphQL 查询。

让 GraphQL 自动缓存

考虑以下查询来获取帖子及其作者

query getPost {
  post(slug: "working-with-graphql-caching") {
    id
    title
    author {
      id
      name
      avatar
    }
  }
}

使 GraphQL 自动缓存起作用的“诀窍”是所有 GraphQL API 都会公开的__typename 元字段。

顾名思义,__typename 返回对象类型的名称。 甚至可以手动将此字段添加到现有查询中 - 大多数时候,GraphQL 客户端或 CDN 会为您完成此操作。 urql 就是这样的一个 GraphQL 客户端。 服务器可能接收类似这样的查询

query getPost {
  post(slug: "working-with-graphql-caching") {
    __typename
    id
    title
    author {
      __typename
      id
      name
    }
  }
}

包含 __typename 的响应可能看起来有点像这样

{
  data: {
    __typename: "Post",
    id: 5,
    title: "Working with GraphQL Caching",
    author: {
      __typename: "User",
      id: 1,
      name: "Jamie Barton"
    }
  }
}

__typename 是 GraphQL 缓存拼图中的关键部分,因为我们现在可以缓存此结果,并且知道它包含帖子 ID 5 和用户 ID 1。

然后有一些库,例如 ApolloRelay,它们也具有一些我们可以用于自动缓存的内置缓存。 由于它们已经知道缓存中有什么,因此它们可以委托给缓存而不是远程 API 来获取客户端在查询中请求的内容。

在发生更改时自动使缓存失效

假设帖子的作者使用editPost 变更来编辑帖子的标题

mutation {
  editPost(input: { id: 5, title: "Working with GraphQL Caching" }) {
    id
    title
  }
}

由于 GraphQL 客户端会自动添加 __typename 字段,因此此变更的结果会立即告知缓存帖子 ID 5 已更改,并且任何包含该帖子的缓存查询结果都需要失效

{
  data: {
    __typename: "Post",
    id: 5,
    title: "Working with GraphQL Caching"
  }
}

下次用户发送相同的查询时,该查询会从源头获取新数据,而不是从缓存中提供陈旧的结果。 太神奇了!

规范化的 GraphQL 缓存

许多 GraphQL 客户端不会缓存完整的查询结果。

相反,它们会将缓存数据规范化为两个数据结构;一个将每个对象与其数据关联起来(例如 帖子 #5: { … }用户 #1: { … } 等);另一个将每个查询与其包含的对象关联起来(例如 获取帖子: { 帖子 #5, 用户 #1} 等)。

请查看 urql 关于规范化缓存的文档Apollo 的“深入了解缓存规范化” 以获取特定示例和用例。

GraphQL 缓存的边缘情况

GraphQL 缓存无法自动处理的一个主要边缘情况是向列表中添加项目。 因此,如果创建帖子 变更通过缓存,它不知道要将该项目添加到哪个特定列表中。

最简单的“解决方法”是在变更中查询父类型(如果存在)。 例如,在下面的查询中,我们查询了帖子 上的社区 关系

query getPost {
  post(slug: "working-with-graphql-caching") {
    id
    title
    author {
      id
      name
      avatar
    }

    # Also query the community of the post
    community {
      id
      name
    }
  }
}

然后我们也可以从创建帖子 变更中查询该社区,并使任何包含该社区的缓存查询结果失效

mutation createPost {
  createPost(input: { ... }) {
    id
    title

    # Also query and thus invalidate the community of the post
    community {
      id
      name
    }
  }
}

虽然不完美,但类型化的模式和__typename 元字段是使 GraphQL API 成为缓存理想选择的关键。

您现在可能在想,所有这些都是一种变通方法,GraphQL 仍然不支持传统的缓存。 你并没有错。 由于 GraphQL 通过 POST 请求运行,您需要将它卸载到应用程序客户端,并使用上述“技巧”来利用 GraphQL 与现代浏览器缓存的优势。

也就是说,这种操作并不总是可行,而且对于管理客户端上的缓存来说也没有太多意义。 GraphQL 客户端让您 手动更新缓存 以处理更棘手的情况,但像 GraphCDN 这样的服务提供了“类似服务器”的缓存体验,它还公开了一个 手动清除 API,使您可以执行类似的操作来实现更好的缓存控制

# Purge all occurrences of a specific object
mutation {
  purgeUser(id: [5])
}
# Purge by query name
mutation {
  _purgeQuery(queries: [listUsers, listPosts])
}
# Purge all occurrences of a type
mutation {
  purgeUser
}

现在,无论您在何处使用 GraphCDN 端点,都不需要再在所有客户端逻辑(移动设备、网络等)中重新实现缓存策略。 边缘缓存使您的 API 感觉超级快,并通过在您的用户之间共享缓存来减少负载,并将缓存与每个用户的客户端隔离开。

最近在一个项目中使用了 GraphCDN,它帮我处理了在客户端或服务器上配置缓存的问题,让我能够继续进行我的项目。 例如,我可以将我的端点替换为 GraphCDN 并免费获得查询复杂度(即将推出)、分析等。


那么,GraphQL 是否关心缓存? 它当然关心! 它不仅提供了内置于其中的某些自动缓存方法,而且许多 GraphQL 库还提供了其他方法来执行缓存甚至管理缓存。

希望这篇文章能让你对 GraphQL 缓存故事有所了解,以及如何在客户端实现它,以及如何利用 CDN 来为你完成所有工作。 我的目标不是让你在所有项目中都使用 GraphQL,但如果你在查询语言之间进行选择,而缓存是一个大问题,那么请知道 GraphQL 完全能够胜任这项任务。