经过验证后,GraphQL 查询将由 GraphQL 服务器执行,该服务器返回一个反映请求查询形状的结果,通常以 JSON 格式。
GraphQL 无法在没有类型系统的情况下执行查询,让我们使用一个示例类型系统来说明执行查询。这是这些文章中所有示例中使用的相同类型系统的一部分。
type Query { human(id: ID!): Human}
type Human { name: String appearsIn: [Episode] starships: [Starship]}
enum Episode { NEWHOPE EMPIRE JEDI}
type Starship { name: String}
为了描述执行查询时发生的情况,让我们使用一个示例来逐步说明。
您可以将 GraphQL 查询中的每个字段视为前一个类型的函数或方法,它返回下一个类型。事实上,这正是 GraphQL 的工作原理。每个类型上的每个字段都由一个名为解析器的函数支持,该函数由 GraphQL 服务器开发人员提供。当执行字段时,将调用相应的解析器来生成下一个值。
如果字段生成一个标量值(如字符串或数字),则执行完成。但是,如果字段生成一个对象值,则查询将包含应用于该对象的另一个字段选择。这将一直持续到到达标量值为止。GraphQL 查询始终以标量值结束。
在每个 GraphQL 服务器的顶层,都有一个类型代表所有可能的 GraphQL API 入口点,它通常被称为根类型或查询类型。
在本例中,我们的查询类型提供了一个名为human
的字段,它接受参数id
。该字段的解析器函数可能会访问数据库,然后构建并返回一个Human
对象。
Query: { human(obj, args, context, info) { return context.db.loadHumanByID(args.id).then( userData => new Human(userData) ) }}
此示例是用 JavaScript 编写的,但是 GraphQL 服务器可以用多种不同的语言构建。解析器函数接收四个参数
obj
前一个对象,对于根查询类型上的字段,通常不使用。args
在 GraphQL 查询中提供给该字段的参数。context
是一个传递给每个解析器的值,它包含重要的上下文信息,例如当前登录的用户或访问数据库的权限。info
是一个包含与当前查询相关的特定于字段的信息以及模式详细信息的值,还可以参考 type GraphQLResolveInfo 获取更多详细信息。让我们仔细看看这个解析器函数中发生了什么。
human(obj, args, context, info) { return context.db.loadHumanByID(args.id).then( userData => new Human(userData) )}
context
用于提供对数据库的访问权限,该数据库用于根据 GraphQL 查询中提供的 id
加载用户数据。由于从数据库加载是异步操作,因此它返回一个 Promise。在 JavaScript 中,Promise 用于处理异步值,但许多语言中都存在相同的概念,通常称为 Futures、Tasks 或 Deferred。当数据库返回时,我们可以构造并返回一个新的 Human
对象。
请注意,虽然解析器函数需要了解 Promise,但 GraphQL 查询不需要。它只是期望 human
字段返回一些东西,然后它可以询问 name
。在执行过程中,GraphQL 将等待 Promise、Futures 和 Tasks 完成,然后继续执行,并且将以最佳并发性执行。
现在 Human
对象可用,GraphQL 执行可以继续处理其上请求的字段。
Human: { name(obj, args, context, info) { return obj.name }}
GraphQL 服务器由类型系统驱动,该类型系统用于确定下一步该做什么。即使在 human
字段返回任何内容之前,GraphQL 也知道下一步将是解析 Human
类型上的字段,因为类型系统告诉它 human
字段将返回一个 Human
。
在这种情况下,解析名称非常简单。调用名称解析器函数,obj
参数是之前字段返回的 new Human
对象。在这种情况下,我们期望 Human 对象具有一个 name
属性,我们可以读取并直接返回。
事实上,许多 GraphQL 库将允许您省略这种简单的解析器,并且将假设如果未为字段提供解析器,则应读取和返回相同名称的属性。
在解析 name
字段的同时,appearsIn
和 starships
字段可以并发解析。appearsIn
字段也可以有一个简单的解析器,但让我们仔细看看
Human: { appearsIn(obj) { return obj.appearsIn // returns [ 4, 5, 6 ] }}
请注意,我们的类型系统声称 appearsIn
将返回具有已知值的枚举值,但是此函数正在返回数字!事实上,如果我们向上查看结果,我们会看到正在返回适当的枚举值。这是怎么回事?
这是一个标量强制转换的示例。类型系统知道预期什么,并将解析器函数返回的值转换为符合 API 契约的值。在本例中,我们的服务器上可能定义了一个枚举,它在内部使用数字,例如 4
、5
和 6
,但在 GraphQL 类型系统中将其表示为枚举值。
我们已经看到,当一个字段返回一个列表时会发生什么,例如上面的 appearsIn
字段。它返回了一个枚举值的列表,由于这是类型系统所期望的,因此列表中的每个项目都被强制转换为相应的枚举值。当解析 starships
字段时会发生什么?
Human: { starships(obj, args, context, info) { return obj.starshipIDs.map( id => context.db.loadStarshipByID(id).then( shipData => new Starship(shipData) ) ) }}
此字段的解析器不仅返回一个 Promise,它返回一个列表的 Promise。Human
对象有一个 Starships
标识符列表,它们是这些 Starships
的驾驶员,但我们需要加载所有这些标识符才能获得真正的 Starship 对象。
GraphQL 将等待所有这些 Promise 并发完成,然后继续执行。当剩下一个对象列表时,它将再次并发地继续加载这些项目中的每个项目的 name
字段。
当每个字段被解析时,结果值将被放置到一个键值映射中,字段名称(或别名)作为键,解析后的值作为值。这从查询的底部叶子字段一直向上到根查询类型上的原始字段。这些共同产生一个结构,该结构反映了原始查询,然后可以将其(通常作为 JSON)发送到请求它的客户端。
让我们最后再看一眼原始查询,看看所有这些解析函数是如何生成结果的