GraphQL 服务器的结构与实现(第三部分)

如果你之前编写过 GraphQL 服务器,很可能你已经遇到过传递到解析器中的 info 对象。幸运的是,在大多数情况下,你并不真正需要理解它实际上做什么以及它在查询解析过程中的作用。
然而,在许多边缘情况下,info 对象是造成大量困惑和误解的原因。本文的目标是深入了解 info 对象,并阐明其在 GraphQL 执行 过程中的作用。
本文假设你已经熟悉 GraphQL 查询和 mutation 如何被解析的基本知识。如果你在这方面感觉有点不扎实,你绝对应该查看本系列的前几篇文章:第一部分:GraphQL Schema (必需),第二部分:The Network Layer (可选)
`info` 对象的结构
回顾:GraphQL 解析器的签名
快速回顾一下,当使用 GraphQL.js 构建 GraphQL 服务器时,你有两个主要任务
- 定义你的 GraphQL schema(可以使用 SDL 或纯 JS 对象)
- 对于你的 schema 中的每个字段,实现一个 resolver 函数,该函数知道如何返回该字段的值
一个 resolver 函数接受四个参数(按顺序排列)
parent
:前一个 resolver 调用的结果 (更多信息)。args
:resolver 字段的参数。context
:每个 resolver 可以从中读取/写入的自定义对象。info
:这是我们将在本文中讨论的内容。
这里概述了一个简单的 GraphQL 查询的执行过程以及所属 resolver 的调用。由于二级 resolver的解析是微不足道的,因此实际上不需要实现这些 resolver——它们的返回值由 GraphQL.js 自动推断。

GraphQL resolver 链中 parent
和 args
参数的概述
info
包含查询 AST 和更多执行信息
那些对 info 对象的结构和作用感到好奇的人仍然一头雾水。官方 规范 和 文档 都没有提到它。曾经有一个 GitHub issue 请求对其进行更好的文档记录,但这问题在没有采取任何显著行动的情况下被关闭了。因此,除了深入研究代码之外,别无他法。
从非常高的层面来说,可以声明 info 对象包含传入 GraphQL 查询的 AST。正因如此,resolver 才知道它们需要返回哪些字段。
要了解更多关于查询 AST 的外观,请务必查看 Christian Joudrey 的精彩文章 Life of a GraphQL Query — Lexing/Parsing 以及 Eric Baer 的精彩演讲 GraphQL Under the Hood。
为了理解 info
的结构,让我们看一下它的 Flow 类型定义
这里概述了每个键的快速解释
fieldName
:如前所述,你的 GraphQL schema 中的每个字段都需要由 resolver 支持。fieldName
包含属于当前 resolver 的字段的名称。fieldNodes
:一个数组,其中每个对象代表剩余选择集中的一个字段。returnType
:相应字段的 GraphQL 类型。parentType
:此字段所属的 GraphQL 类型。path
:跟踪已遍历的字段,直到到达当前字段(即 resolver)。schema
:代表你的可执行 schema 的GraphQLSchema
实例。fragments
:query 文档中包含的 fragment 的映射。rootValue
:传递给执行的rootValue
参数。operation
:整个 query 的 AST。variableValues
:与 query 一起提供的任何变量的映射,对应于 variableValues 参数。
如果这仍然显得抽象,请不要担心,我们很快就会看到所有这些的示例。
字段特定 vs 全局
关于上面的键,有一个有趣的观察结果。info
对象上的键要么是字段特定的,要么是全局的。
字段特定 仅仅意味着该键的值取决于传递 info
对象的字段(及其支持 resolver)。明显的例子是 fieldName
、rootType
和 parentType
。考虑以下 GraphQL 类型的 author
字段
该字段的 fieldName
只是 author
,returnType
是 User!
,parentType
是 Query
。
现在,对于 feed
,这些值当然会有所不同:fieldName
是 feed
,returnType
是 [Post!]!
,parentType
也是 Query
。
因此,这三个键的值是字段特定的。进一步的字段特定键是:fieldNodes
和 path
。实际上,上面 Flow 定义的前五个键是字段特定的。
另一方面,全局 意味着这些键的值不会改变——无论我们谈论的是哪个 resolver。schema
、fragments
、rootValue
、operation
和 variableValues
对于所有 resolver 都将始终携带相同的值。
一个简单的例子
现在让我们继续看一个 info
对象内容的例子。为了打好基础,这是我们将用于此示例的 schema 定义
假设该 schema 的 resolver 如下实现
请注意,
Post.title
resolver 实际上不是必需的,我们仍然在此处包含它,以查看在调用 resolver 时info
对象的外观。
现在考虑以下 query
为了简洁起见,我们只讨论 Query.author
字段的 resolver,而不是 Post.title
的 resolver(当执行上述 query 时,后者仍然会被调用)。
如果你想亲自尝试这个例子,我们准备了一个包含上述 schema 运行版本的 存储库,你可以用它来做实验!
接下来,让我们看一下 info
对象中的每个键,看看当调用 Query.author
resolver 时它们是什么样子(你可以在 这里 找到 info
对象的完整日志输出)。
fieldName
fieldName
只是 author
。
fieldNodes
请记住,fieldNodes
是字段特定的。它实际上包含查询 AST 的摘录。此摘录从当前字段(即 author
)开始,而不是从 query 的根开始。(从根开始的整个查询 AST 存储在 operation
中,见下文)。
returnType
& parentType
如前所述,returnType
和 parentType
非常简单
path
path
跟踪已遍历的字段,直到到达当前字段。对于 Query.author
,它看起来很简单,就像 "path": { "key": "author" }
。
为了比较,在 Post.title
resolver 中,path
看起来如下
剩余的五个字段属于“全局”类别,因此对于
Post.title
resolver 而言将是相同的。
schema
schema
是对可执行 schema 的引用。
fragments
fragments
包含 fragment 定义,由于 query 文档没有任何 fragment 定义,因此它只是一个空映射:{}
。
rootValue
如前所述,rootValue
键的值对应于首先传递给 graphql 执行函数的 rootValue
参数。在本例中,它只是 null
。
operation
operation
包含传入 query 的完整 query AST。回想一下,除其他信息外,这还包含我们在上面看到的 fieldNodes
的相同值
variableValues
此键表示为 query 传递的任何变量。由于我们的示例中没有变量,因此该值再次只是一个空映射:{}
。
如果 query 是用变量编写的
variableValues
键将只具有以下值
使用 GraphQL binding 时 info
的作用
正如本文开头所提到的,在大多数情况下,你根本不需要担心 info
对象。它只是恰好是你 resolver 签名的一部分,但你实际上并没有将其用于任何事情。那么,它什么时候变得相关呢?
将 info
传递给 binding 函数
如果你之前使用过 GraphQL binding,你已经看到 info
对象是生成的 binding 函数的一部分。考虑以下 schema
使用 graphql-binding
,你现在可以通过调用专用的 binding 函数而不是发送原始 query 和 mutation 来发送可用的 query 和 mutation。
例如,考虑以下原始 query,检索特定的 User
使用 binding 函数实现相同目的将如下所示
通过在 binding 实例上调用 user
函数并传递相应的参数,我们传达的信息与上面的原始 GraphQL query 完全相同。
来自 graphql-binding
的 binding 函数接受三个参数
args
:包含字段的参数(例如,上面createUser
mutation 的username
)。context
:向下传递 resolver 链的context
对象。info
:info
对象。请注意,除了GraphQLResolveInfo
的实例(info 的类型)之外,你还可以传递一个简单定义选择集的字符串。
使用 Prisma 将应用程序 schema 映射到数据库 schema
另一个 info
对象可能引起混淆的常见用例是基于 Prisma 和 prisma-binding 实现 GraphQL 服务器。
在这种情况下,想法是拥有两个 GraphQL 层
- 数据库层 由 Prisma 自动生成,并提供通用且强大的 CRUD API
- 应用程序层 定义暴露给客户端应用程序并根据你的应用程序需求量身定制的 GraphQL API
作为后端开发人员,你负责为应用程序层定义应用程序 schema 并实现其 resolver。由于 prisma-binding
,resolver 的实现仅仅是将传入 query 委托给底层数据库 API 的过程,而不会产生重大开销。
让我们考虑一个简单的例子——假设你从以下 Prisma 数据库服务的数据模型开始
Prisma 基于此数据模型生成的数据库 schema 看起来与此类似
现在,假设你想构建一个看起来与此类似的应用程序 schema
feed
query 不仅返回 Post
元素列表,还能够返回列表的 count
。请注意,它可以选择接受一个 authorId
,该 authorId
会过滤 feed 以仅返回由特定 User
编写的 Post
元素。
实现此应用程序 schema 的第一直觉可能如下所示。
实现 1:此实现看起来正确,但存在细微缺陷
此实现看起来足够合理。在 feed
resolver 内部,我们基于潜在的传入 authorId
构建 authorFilter
。然后使用 authorFilter
执行 posts
query 并检索 Post
元素,以及 postsConnection
query,后者提供对列表 count
的访问权限。
也可以仅使用 postsConnection query 来检索实际的 *Post* 元素。为了保持简单,我们仍然为此使用 *posts* query,并将另一种方法留给细心的读者作为练习。
实际上,当使用此实现启动你的 GraphQL 服务器时,乍一看事情似乎进展顺利。你会注意到简单的 query 可以正确服务,例如以下 query 将会成功
只有当你尝试检索 Post
元素的 author
时,才会遇到问题
好的!所以,由于某种原因,该实现未返回 author
,这触发了一个错误“Cannot return null for non-nullable Post.author.”,因为 Post.author
字段在应用程序 schema 中被标记为必需。
让我们再次看一下实现的相关部分
这是我们检索 Post 元素的地方。但是,我们没有将选择集传递给 posts binding 函数。如果未将第二个参数传递给 Prisma binding 函数,则默认行为是查询该类型的所有标量字段。
这确实解释了这种行为。对 ctx.db.query.posts
的调用返回了正确的 Post
元素集,但仅返回了它们的 id
和 title
值——没有关于 author
的关系数据。
那么,我们如何解决这个问题呢?显然需要一种方法来告诉 posts
binding 函数它需要返回哪些字段。但是该信息驻留在 feed
resolver 的上下文中的哪个位置呢?你能猜到吗?
正确:在 info
对象内部!因为 Prisma binding 函数的第二个参数可以是字符串或 info
对象,所以让我们只将传递到 feed
resolver 的 info
对象传递给 posts
binding 函数。
此 query 使用实现 2 失败:“类型为 'Post' 的字段 'posts' 必须具有子选择。”
然而,使用此实现,任何请求都无法正确服务。例如,考虑以下 query
错误消息“类型为 'Post' 的字段 'posts' 必须具有子选择。”是由上面实现的第 8 行产生的。
那么,这里发生了什么?失败的原因是 info
对象中字段特定的键与 posts
query 不匹配。
在 feed
resolver 内部打印 info
对象可以更清楚地了解情况。让我们仅考虑 fieldNodes
中字段特定的信息
此 JSON 对象也可以表示为字符串选择集
现在一切都说得通了!我们将上面的选择集发送到 Prisma 数据库 schema 的 posts
query,当然该 schema 不知道 feed
和 count
字段。诚然,产生的错误消息不是很有帮助,但至少我们现在明白了发生了什么。
那么,解决这个问题的方法是什么?解决此问题的一种方法是手动解析出 fieldNodes
选择集的正确部分,并将其传递给 posts
binding 函数(例如,作为字符串)。
然而,这个问题有一个更优雅的解决方案,那就是为应用程序 schema 中的 Feed
类型实现专用 resolver。以下是正确的实现方式。
实现 3:此实现修复了上述问题
此实现修复了上面讨论的所有问题。有几点需要注意
- 在第 8 行中,我们现在将字符串选择集(
{ id }
)作为第二个参数传递。这只是为了提高效率,因为否则将获取所有标量值(这在我们的示例中不会产生巨大差异),而我们只需要 ID。 - 我们没有从
Query.feed
resolver 返回posts
,而是返回postIds
,它只是一个 ID 数组(表示为字符串)。 - 在
Feed.posts
resolver 中,我们现在可以访问由 parent resolver 返回的postIds
。这次,我们可以利用传入的info
对象并简单地将其传递给posts
binding 函数。
如果你想亲自尝试这个例子,你可以查看 这个 存储库,其中包含上述例子的运行版本。随意尝试本文中提到的不同实现,并亲自观察行为!
总结
在本文中,你深入了解了在基于 GraphQL.js 实现 GraphQL API 时使用的 info
对象。
info
对象没有官方文档——要了解更多关于它的信息,你需要深入研究代码。在本教程中,我们首先概述了其内部结构,并理解了其在 GraphQL resolver 函数中的作用。然后,我们介绍了一些边缘情况和潜在陷阱,在这些情况下,需要更深入地理解 info
。
本文展示的所有代码都可以在相应的 GitHub 存储库中找到,因此你可以亲自尝试并观察 info 对象的行为。
不要错过下一篇文章!
注册 Prisma 新闻通讯