2018年2月6日

GraphQL 基础知识:揭秘 GraphQL 解析器中的 info 参数

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

GraphQL Basics

如果你写过 GraphQL 服务器,很可能已经遇到过传递给解析器的 info 对象。幸运的是,在大多数情况下,你并不需要真正理解它实际做了什么以及它在查询解析过程中扮演的角色。

然而,在许多边缘情况下,info 对象会引起很多困惑和误解。本文的目标是深入探讨 info 对象的内部结构,并阐明它在 GraphQL 执行过程中的作用。

本文假设您已经熟悉 GraphQL 查询和变更(mutations)是如何解析的基础知识。如果您在这方面有些不确定,您应该阅读本系列的先前文章:第一部分:GraphQL Schema(必读)第二部分:网络层(选读)

info 对象的结构

回顾:GraphQL 解析器的签名

快速回顾一下,使用 GraphQL.js 构建 GraphQL 服务器时,您有两个主要任务:

  • 定义您的 GraphQL schema(可以是 SDL 或纯 JS 对象)
  • 为 schema 中的每个字段,实现一个知道如何返回该字段值的解析器函数

解析器函数接受四个参数(按此顺序):

  1. parent:前一个解析器调用的结果(更多信息)。
  2. args:解析器字段的参数。
  3. context:每个解析器都可以读取/写入的自定义对象。
  4. info这就是我们将在本文中讨论的内容。

以下是简单 GraphQL 查询的执行过程以及相关解析器调用的概述。由于第二级解析器的解析很简单,因此无需实际实现这些解析器——它们的返回值由 GraphQL.js 自动推断。

GraphQL 解析器链中 parentargs 参数概述

info 包含查询 AST 及更多执行信息

那些对 info 对象的结构和作用感到好奇的人对此一无所知。官方规范文档都没有提及它。以前有一个 GitHub issue 请求为其提供更好的文档,但该 issue 已被关闭,没有采取任何显著行动。因此,除了深入研究代码之外别无他法。

从非常高层次来看,可以说 info 对象包含了传入 GraphQL 查询的 AST。得益于此,解析器知道需要返回哪些字段。

要了解更多关于查询 AST 的信息,请务必阅读 Christian Joudrey 的精彩文章 Life of a GraphQL Query — Lexing/Parsing,以及 Eric Baer 的出色演讲 GraphQL Under the Hood

为了理解 info 的结构,我们来看看 其 Flow 类型定义

以下是这些键的概述和简要解释:

  • fieldName:如前所述,GraphQL schema 中的每个字段都需要由一个解析器支持。 fieldName 包含属于当前解析器的字段名称。
  • fieldNodes:一个数组,其中每个对象代表剩余选择集中的一个字段。
  • returnType:相应字段的 GraphQL 类型。
  • parentType:此字段所属的 GraphQL 类型。
  • path:跟踪直到当前字段(即解析器)所遍历的字段。
  • schema:代表您的可执行 schema 的 GraphQLSchema 实例。
  • fragments:查询文档中包含的片段(fragments)映射。
  • rootValue:传递给执行的 rootValue 参数。
  • operation整个查询的 AST。
  • variableValues:与查询一起提供的任何变量的映射,对应于 variableValues 参数。

如果这仍然显得抽象,请不要担心,我们很快会看到所有这些的示例。

字段特定 vs 全局

关于上面的键,有一个有趣的观察。 info 对象上的键要么是字段特定的,要么是全局的。

字段特定简单来说意味着该键的值取决于 info 对象被传递到的字段(及其支持的解析器)。明显的例子包括 fieldNamerootTypeparentType。考虑以下 GraphQL 类型的 author 字段:

该字段的 fieldName 只是 authorreturnTypeUser!parentTypeQuery

现在,对于 feed,这些值当然会有所不同:fieldNamefeedreturnType[Post!]!parentType 也是 Query

因此,这三个键的值是字段特定的。其他字段特定的键包括:fieldNodespath。实际上,上面 Flow 定义的前五个键都是字段特定的。

另一方面,全局意味着这些键的值不会改变——无论我们谈论的是哪个解析器。 schemafragmentsrootValueoperationvariableValues 对所有解析器来说始终具有相同的值。

一个简单的例子

现在让我们来看一个 info 对象内容的例子。为了铺垫,以下是我们将在此示例中使用的 schema 定义

假设该 schema 的解析器实现如下:

注意,Post.title 解析器并非必需,我们将其包含在此处是为了查看解析器被调用时 info 对象的样子。

现在考虑以下查询:

为了简洁起见,我们只讨论 Query.author 字段的解析器,而不是 Post.title 的解析器(尽管上面的查询执行时仍然会调用它)。

如果您想亲自尝试这个例子,我们准备了一个包含上述 schema 运行版本的 仓库,供您实验!

接下来,让我们看看 info 对象中的每个键,并了解当 Query.author 解析器被调用时它们的样子(您可以在此处找到 info 对象的完整日志输出)。

fieldName

fieldName 简单来说就是 author

fieldNodes

请记住,fieldNodes 是字段特定的。它实际上包含了查询 AST 的一个摘录。这个摘录从当前字段(即 author)开始,而不是从查询的开始。(从根开始的整个查询 AST 存储在 operation 中,见下文)。

returnTypeparentType

如前所述,returnTypeparentType 相当直观:

path

path 跟踪直到当前字段所遍历的字段。对于 Query.author,它看起来很简单:"path": { "key": "author" }

为了进行比较,在 Post.title 解析器中,path 如下所示:

剩下的五个字段属于“全局”类别,因此对于 Post.title 解析器来说将是相同的。

schema

schema 是对可执行 schema 的引用。

fragments

fragments 包含片段定义,由于查询文档中没有任何片段,因此它只是一个空映射:{}

rootValue

如前所述,rootValue 键的值对应于最初传递给 graphql 执行函数的 rootValue 参数。在本例中,它只是 null

operation

operation 包含传入查询的完整查询 AST。回忆一下,除了其他信息,这包含了我们上面看到的 fieldNodes 的相同值:

variableValues

这个键代表为查询传递的任何变量。由于我们的示例中没有变量,因此该值仍然只是一个空映射:{}

如果查询是带变量写的:

variableValues 键的值将是:

使用 GraphQL 绑定时 info 的作用

正如本文开头所述,在大多数情况下,您完全不必担心 info 对象。它只是恰好是解析器签名的一部分,但您实际上并不会使用它做任何事情。那么,它何时变得相关呢?

info 传递给绑定函数

如果您之前使用过GraphQL 绑定,您会看到 info 对象作为生成的绑定函数的一部分。考虑以下 schema:

使用 graphql-binding,您现在可以通过调用专门的绑定函数来发送可用的查询和变更,而不是发送原始的查询和变更。

例如,考虑以下原始查询,用于检索特定的 User

使用绑定函数实现相同功能如下所示:

通过在绑定实例上调用 user 函数并传递相应的参数,我们传达的信息与上面的原始 GraphQL 查询完全相同。

graphql-binding 中的绑定函数接受三个参数:

  1. args:包含字段的参数(例如,上面 createUser 变更的 username)。
  2. context:沿解析器链传递的 context 对象。
  3. infoinfo 对象。注意,除了传递 GraphQLResolveInfo 实例(即 info 的类型),您还可以传递一个简单定义选择集的字符串。

使用 Prisma 将应用 schema 映射到数据库 schema

另一个 info 对象可能引起混淆的常见用例是基于 Prismaprisma-binding 实现 GraphQL 服务器。

在这种背景下,想法是拥有两个 GraphQL 层:

  • 数据库层 由 Prisma 自动生成,提供通用且强大的 CRUD API
  • 应用层 定义了暴露给客户端应用程序的 GraphQL API,并根据您的应用程序需求进行了定制

作为后端开发人员,您负责为应用层定义应用 schema 并实现其解析器。得益于 prisma-binding,解析器的实现仅仅是将传入查询委托给底层数据库 API 的过程,没有带来太大开销。

让我们考虑一个简单的例子——假设您从 Prisma 数据库服务的以下数据模型开始:

Prisma 基于此数据模型生成的数据库 schema 看起来类似于:

现在,假设您想构建一个看起来类似于这样的应用 schema:

feed 查询不仅返回 Post 元素的列表,还能返回该列表的 count。注意,它可选地接受一个 authorId,用于过滤 feed,使其只返回由特定 User 编写的 Post 元素。

实现此应用 schema 的第一种直觉可能如下所示。

实现 1:此实现看起来正确,但存在一个微小的缺陷

此实现看起来相当合理。在 feed 解析器内部,我们根据可能传入的 authorId 构建 authorFilter。然后使用 authorFilter 执行 posts 查询以检索 Post 元素,以及 postsConnection 查询以获取列表的 count

使用 postsConnection 查询也可以检索实际的 Post 元素。为了保持简单,我们仍然使用 posts 查询来做这件事,并将另一种方法留给细心的读者作为练习。

事实上,当您使用此实现启动 GraphQL 服务器时,乍一看一切似乎都正常。您会注意到简单的查询能正常提供服务,例如以下查询会成功执行:

直到您尝试检索 Post 元素的 author 时,才会遇到问题:

好吧!所以,由于某种原因,实现没有返回 author,这触发了一个错误:“Cannot return null for non-nullable Post.author.” (不能为非空 Post.author 返回 null),因为 Post.author 字段在应用 schema 中被标记为必需。

让我们再次看看实现的相关部分:

这里是我们检索 Post 元素的地方。然而,我们没有将一个选择集传递给 posts 绑定函数。如果没有向 Prisma 绑定函数传递第二个参数,默认行为是查询该类型的所有标量字段。

这确实解释了这种行为。ctx.db.query.posts 的调用返回了正确的 Post 元素集,但只返回了它们的 idtitle 值——没有关于 author 的关系数据。

那么,我们如何解决这个问题呢?显然,需要一种方法来告诉 posts 绑定函数需要返回哪些字段。但这些信息存储在 feed 解析器的上下文中吗?您能猜到吗?

正确答案:info 对象内部!由于 Prisma 绑定函数的第二个参数可以是字符串一个 info 对象,我们只需将传递给 feed 解析器的 info 对象直接传递给 posts 绑定函数。

查询在实现 2 中失败,错误信息是:“Field ‘posts’ of type ‘Post’ must have a sub selection.” (类型‘Post’的字段‘posts’必须有子选择)。

然而,使用此实现,任何请求都将无法正常提供服务。例如,考虑以下查询:

错误消息“Field ‘posts’ of type ‘Post’ must have a sub selection.” 由上面实现的第 8 行产生。

那么,这里发生了什么?失败的原因是因为 info 对象中的字段特定键与 posts 查询不匹配。

feed 解析器内部打印 info 对象有助于更清楚地了解情况。让我们只考虑 fieldNodes 中的字段特定信息:

这个 JSON 对象也可以表示为一个字符串选择集:

现在一切都说得通了!我们将上面的选择集发送到 Prisma 数据库 schema 的posts 查询,这个查询当然不知道 feedcount 字段。不得不承认,产生的错误消息不是很有帮助,但至少我们现在明白了是怎么回事。

那么,这个问题有什么解决方案呢?解决这个问题的一种方法是手动解析出 fieldNodes 的选择集中正确的部分,并将其传递给 posts 绑定函数(例如,作为字符串)。

然而,有一个更优雅的解决方案,那就是为应用 schema 中的 Feed 类型实现专门的解析器。以下是正确的实现方式:

实现 3:此实现解决了上述问题

此实现解决了上面讨论的所有问题。有几点需要注意:

  • 第 8 行,我们现在传递一个字符串选择集({ id })作为第二个参数。这只是为了效率,因为否则会获取所有标量值(这在我们的例子中不会产生巨大差异),而我们只需要 ID。
  • 我们不是从 Query.feed 解析器返回 posts,而是返回 postIds,它只是一个 ID 数组(表示为字符串)。
  • Feed.posts 解析器中,我们现在可以访问由解析器返回的 postIds。这次,我们可以利用传入的 info 对象,并将其直接传递给 posts 绑定函数。

如果您想亲自尝试这个例子,您可以查看这个仓库,其中包含上述示例的运行版本。请随意尝试本文中提到的不同实现,并亲自观察其行为!

总结

在本文中,您深入了解了基于 GraphQL.js 实现 GraphQL API 时使用的 info 对象。

info 对象没有官方文档——要了解更多信息,您需要深入研究代码。在本教程中,我们首先概述了其内部结构并理解了它在 GraphQL 解析器函数中的作用。然后,我们介绍了一些需要更深入理解 info 的边缘情况和潜在陷阱。

本文展示的所有代码都可以在相应的 GitHub 仓库中找到,您可以亲自实验并观察 info 对象的行为。

不要错过下一篇文章!

订阅 Prisma 新闻通讯