2018年2月6日

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

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

GraphQL Basics

如果您以前编写过 GraphQL 服务器,那么很可能已经遇到过传递给解析器的 `info` 对象。幸运的是,在大多数情况下,您并不需要真正理解它到底做了什么以及它在查询解析过程中的作用。

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

本文假设您已经熟悉 GraphQL 查询和变更如何解析的基础知识。如果您对此感到有些不确定,您应该查阅本系列的前几篇文章:第一部分:GraphQL 模式(必读)第二部分:网络层(可选)

`info` 对象的结构

回顾:GraphQL 解析器的签名

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

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

一个解析器函数接收四个参数(按此顺序)

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

这里是一个简单 GraphQL 查询的执行过程以及所属解析器调用的概览。因为*第二层解析器*的解析是微不足道的,所以实际上没有必要实现这些解析器——它们的返回值由 GraphQL.js 自动推断。

Blog image

GraphQL 解析器链中 `parent` 和 `args` 参数的概览

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

那些对 `info` 对象的结构和作用感到好奇的人,却发现信息匮乏。无论是官方的规范,还是文档,都没有提及它。以前有一个 GitHub 议题要求更好地记录它,但那个议题在没有显著进展的情况下被关闭了。所以,除了深入代码之外,别无他法。

从非常高层的角度来看,可以说 `info` 对象包含了传入 GraphQL 查询的 AST。因此,解析器知道它们需要返回哪些字段。

要了解更多关于查询 AST 的样子,请务必查看 Christian Joudrey 的精彩文章 GraphQL 查询的生命周期——词法分析/解析,以及 Eric Baer 的精彩演讲 GraphQL 幕后

要理解 `info` 的结构,我们来看看它的 Flow 类型定义

以下是这些键的概览和快速解释

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

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

字段特定 vs 全局

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

*字段特定*简单地意味着该键的值取决于将 `info` 对象传递到的字段(及其支持的解析器)。显而易见的例子是 `fieldName`、`returnType` 和 `parentType`。考虑以下 GraphQL 类型中的 `author` 字段

该字段的 `fieldName` 就是 `author`,`returnType` 是 `User!`,`parentType` 是 `Query`。

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

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

*全局*则意味着这些键的值不会改变——无论我们讨论的是哪个解析器。`schema`、`fragments`、`rootValue`、`operation` 和 `variableValues` 对于所有解析器来说都将始终携带相同的值。

一个简单示例

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

假设该模式的解析器实现如下

请注意,`Post.title` 解析器实际上不是必需的,但我们仍将其包含在此处,以便查看解析器被调用时 `info` 对象的样子。

现在考虑以下查询

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

如果您想尝试此示例,我们准备了一个仓库,其中包含上述模式的运行版本,供您进行实验!

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

`fieldName`

`fieldName` 简单地是 `author`。

`fieldNodes`

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

`returnType` & `parentType`

如前所述,`returnType` 和 `parentType` 相当简单

`path`

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

相比之下,在 `Post.title` 解析器中,`path` 如下所示

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

`schema`

`schema` 是对可执行模式的引用。

`fragments`

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

`rootValue`

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

`operation`

`operation` 包含传入查询的完整查询 AST。回想一下,除了其他信息之外,这还包含我们上面为 `fieldNodes` 看到的值。

`variableValues`

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

如果查询是使用变量编写的

`variableValues` 键将简单地具有以下值

使用 GraphQL 绑定时 `info` 的作用

如本文开头所述,在大多数情况下,您根本不需要担心 `info` 对象。它只是作为解析器签名的一部分存在,但您实际上并未将其用于任何目的。那么,它何时变得相关呢?

将 `info` 传递给绑定函数

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

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

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

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

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

一个来自 `graphql-binding` 的绑定函数接受三个参数

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

使用 Prisma 将应用程序模式映射到数据库模式

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

在这种情况下,我们的想法是拥有两个 GraphQL 层

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

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

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

Prisma 根据此数据模型生成的数据库模式看起来与此类似

现在,假设您想构建一个类似的应用程序模式

`feed` 查询不仅返回 `Post` 元素的列表,还能够返回列表的 `count`。请注意,它可以选择性地接受 `authorId`,用于过滤动态以仅返回特定 `User` 编写的 `Post` 元素。

实现此应用程序模式的第一个直觉可能如下所示。

实现 1:此实现看起来正确,但有一个细微的缺陷

此实现看起来足够合理。在 `feed` 解析器内部,我们根据可能传入的 `authorId` 构建 `authorFilter`。然后 `authorFilter` 用于执行 `posts` 查询并检索 `Post` 元素,以及 `postsConnection` 查询,后者提供对列表 `count` 的访问。

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

事实上,当您使用此实现启动 GraphQL 服务器时,乍一看一切都会顺利。您会注意到简单的查询会被正确处理,例如以下查询将会成功:

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

好吧!所以,由于某种原因,此实现没有返回 `author`,这触发了一个错误“无法为非空 `Post.author` 返回 null。”因为 `Post.author` 字段在*应用程序模式*中被标记为必需。

我们再来看看实现的相关部分

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

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

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

正确:在 `info` 对象内部!因为 Prisma 绑定函数的第二个参数可以是字符串*或* `info` 对象,所以我们只需将传入 `feed` 解析器的 `info` 对象传递给 `posts` 绑定函数。

此查询因实现 2 而失败:“类型 'Post' 的字段 'posts' 必须具有子选择。”

然而,使用此实现,*没有*请求会被正确处理。举例来说,考虑以下查询:

错误消息“类型 'Post' 的字段 'posts' 必须具有子选择。”是由上述实现的*第 8 行*产生的。

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

在 `feed` 解析器内部打印 `info` 对象可以更清楚地说明情况。我们只考虑 `fieldNodes` 中字段特定的信息

此 JSON 对象也可以表示为字符串选择集

现在一切都说得通了!我们将上述选择集发送到 Prisma 数据库模式的 posts 查询,该查询当然不知道 `feed` 和 `count` 字段。诚然,产生的错误消息并不是非常有用,但至少我们现在明白了发生了什么。

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

然而,有一个更优雅的解决方案,那就是为应用程序模式中的 `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 新闻通讯

© . All rights reserved.