2017 年 11 月 14 日

GraphQL 服务器基础知识:GraphQL 模式、TypeDefs 和解析器详解

GraphQL 服务器的结构和实现(第一部分)

GraphQL Server Basics

当开始使用 GraphQL 时,首先要问的问题之一是如何构建 GraphQL 服务器?由于 GraphQL 只是作为规范发布,因此您的 GraphQL 服务器实际上可以使用您喜欢的任何编程语言实现

在开始构建服务器之前,GraphQL 要求您设计一个模式,该模式反过来定义服务器的 API。在这篇文章中,我们希望了解模式的主要组成部分,阐明实际实现它的机制,并了解诸如 GraphQL.jsgraphql-toolsgraphene-js 等库如何在过程中为您提供帮助。

本文仅涉及 纯 GraphQL 功能——没有定义服务器如何与客户端通信的网络层概念。重点是“GraphQL 执行引擎”的内部工作原理和查询解析过程。要了解网络层,请查看下一篇文章

GraphQL 模式定义了服务器的 API

定义模式:模式定义语言

GraphQL 有自己的类型语言,用于编写 GraphQL 模式:模式定义语言 (SDL)。在其最简单的形式中,GraphQL SDL 可用于定义如下类型的类型

User 类型本身不向客户端应用程序公开任何功能,它只是定义应用程序中用户模型的结构。为了向 API 添加功能,您需要将字段添加到 GraphQL 模式的根类型QueryMutationSubscription。这些类型定义了 GraphQL API 的入口点

例如,考虑以下查询

只有当相应的 GraphQL 模式定义了具有以下 user 字段的 Query 根类型时,此查询才有效

因此,模式的根类型决定了服务器将接受的查询和变更的形状。

GraphQL 模式为客户端-服务器通信提供了清晰的约定。

GraphQLSchema 对象是 GraphQL 服务器的核心

GraphQL.js 是 Facebook 的 GraphQL 参考实现,为其他库(如 graphql-toolsgraphene-js)提供了基础。当使用任何这些库时,您的开发过程都围绕 GraphQLSchema 对象展开,该对象由两个主要组件组成

  • 模式定义
  • 实际的实现,以解析器函数的形式

对于上面的示例,GraphQLSchema 对象如下所示

如您所见,模式的 SDL 版本可以直接转换为 GraphQLSchema 类型的 JavaScript 表示形式。请注意,此模式没有任何解析器——因此它不允许您实际执行任何查询或变更。有关更多信息,请参见下一节。

解析器实现 API

GraphQL 服务器中的结构与行为

GraphQL 清晰地分离了结构行为。正如我们刚刚讨论的那样,GraphQL 服务器的结构是其模式,即服务器功能的抽象描述。这种结构通过具体的实现而变得生动,该实现决定了服务器的行为。实现的关键组件是所谓的解析器函数。

GraphQL 模式中的每个字段都由一个解析器支持。

在其最基本的形式中,GraphQL 服务器在其模式中的每个字段都将具有一个解析器函数。每个解析器都知道如何获取其字段的数据。由于 GraphQL 查询本质上只是字段的集合,因此 GraphQL 服务器为了收集请求的数据实际上只需要调用查询中指定的字段的所有解析器函数。(这也是为什么 GraphQL 经常与 RPC 风格的系统进行比较的原因,因为它本质上是一种用于调用远程函数的语言。)

解析器函数的剖析

当使用 GraphQL.js 时,GraphQLSchema 对象中类型的每个字段都可以附加一个 resolve 函数。让我们考虑一下上面的示例,特别是 Query 类型上的 user 字段——在这里我们可以添加一个简单的 resolve 函数,如下所示

假设函数 fetchUserById 实际上可用并返回 User 实例(具有 idname 字段的 JS 对象),则 resolve 函数现在可以执行模式。

在深入探讨之前,让我们花一点时间来了解传递到解析器中的四个参数

  1. root(有时也称为 parent):还记得我们说过 GraphQL 服务器为了解析查询需要做的就是调用查询字段的解析器吗?好吧,它是广度优先(逐级)执行的,并且每个解析器调用中的 root 参数只是前一次调用的结果(如果未另行指定,则初始值为 null)。
  2. args:此参数携带查询的参数,在本例中是要获取的 Userid
  3. context:一个对象,它在解析器链中传递,每个解析器都可以写入和读取(基本上是解析器之间通信和共享信息的一种方式)。
  4. info:查询或变更的 AST 表示形式。您可以在本系列的第三部分中阅读有关详细信息的更多信息:解密 GraphQL 解析器中的 info 参数

之前我们说过GraphQL 模式中的每个字段都由一个解析器函数支持。目前我们只有一个解析器,而我们的模式总共有三个字段:Query 类型上的根字段 user,以及 User 类型上的 idname 字段。其余两个字段仍然需要它们的解析器。您会看到,这些解析器的实现很简单

查询执行

考虑到我们上面的查询,让我们了解它是如何执行和收集数据的。该查询总共包含三个字段:user根字段)、idname。这意味着当查询到达服务器时,服务器需要调用三个解析器函数——每个字段一个。让我们来看一下执行流程

  1. 查询到达服务器。
  2. 服务器调用根字段 user 的解析器——假设 fetchUserById 返回此对象:{ "id": "abc", "name": "Sarah" }
  3. 服务器调用 User 类型上字段 id 的解析器。此解析器的 root 输入参数是前一次调用的返回值,因此它可以简单地返回 root.id
  4. 类似于 3,但最终返回 root.name。(请注意,3 和 4 可以并行发生。)
  5. 解析过程终止——最终结果用 data 字段包装,以符合 GraphQL 规范

现在,您真的需要自己为 user.iduser.name 编写解析器吗?当使用 GraphQL.js 时,如果实现像示例中那样简单,则不必实现解析器。因此,您可以省略它们的实现,因为 GraphQL.js 已经根据字段名称和 root 参数推断出它需要返回的内容。

优化请求:DataLoader 模式

使用上述执行方法,当客户端发送深度嵌套的查询时,很容易遇到性能问题。假设我们的 API 也有文章评论可以请求,并允许此查询

请注意,我们是如何从给定 user 请求特定 article,以及它的 comments 和撰写它们的用户的 name

假设这篇文章有五个评论,全部由同一用户撰写。这意味着我们将五次访问 writtenBy 解析器,但每次都会返回相同的数据。DataLoader 允许您在这些情况下进行优化,以避免 N+1 查询问题——总体思路是解析器调用被批处理,因此数据库(或其他数据源)只需要被访问一次。

要了解有关 DataLoader 的更多信息,您可以观看 Lee Byron 的精彩视频:DataLoader — 源代码演练(约 35 分钟)

GraphQL.js 与 graphql-tools

现在让我们谈谈可用的库,这些库可以帮助您在 JavaScript 中实现 GraphQL 服务器——主要这是关于 GraphQL.js 和 graphql-tools 之间的区别。

GraphQL.js 为 graphql-tools 提供了基础

要理解的第一个关键是 GraphQL.js 为 graphql-tools 提供了基础。它通过定义所需的类型、实现模式构建以及查询验证和解析来完成所有繁重的工作。graphql-tools 然后在 GraphQL.js 之上提供了一个薄的便利层。

让我们快速浏览一下 GraphQL.js 提供的功能。请注意,其功能通常围绕 GraphQLSchema 展开

  • parsebuildASTSchema:给定一个在 GraphQL SDL 中定义为字符串的 GraphQL 模式,这两个函数将创建一个 GraphQLSchema 实例:const schema = buildASTSchema(parse(sdlString))
  • validate:给定一个 GraphQLSchema 实例和一个查询,validate 确保查询符合模式定义的 API。
  • execute:给定一个 GraphQLSchema 实例和一个查询,execute 调用查询字段的解析器,并根据 GraphQL 规范创建响应。当然,这仅在解析器是 GraphQLSchema 实例的一部分时才有效(否则它只是一个有菜单但没有厨房的餐厅)。
  • printSchema:接受一个 GraphQLSchema 实例,并以 SDL(作为字符串)返回其定义。

请注意,GraphQL.js 中最重要的函数是 graphql,它接受一个 GraphQLSchema 实例和一个查询——然后调用 validateexecute

要了解所有这些函数,请查看这个简单的 node 脚本,它在一个简单的示例中使用了它们。

graphql 函数针对本身已经包含结构行为的模式执行 GraphQL 查询。graphql 的主要作用是协调解析器函数的调用,并根据提供的查询的形状打包响应数据。在这方面,graphql 函数实现的功能也称为 GraphQL 引擎

graphql-tools:桥接接口和实现

使用 GraphQL 的好处之一是您可以采用模式优先的开发过程,这意味着您构建的每个功能首先都体现在 GraphQL 模式中——然后通过相应的解析器实现。这种方法有很多好处,例如,它允许前端开发人员在后端开发人员实际实现 API 之前,开始针对模拟 API 进行工作——这要归功于 SDL。

GraphQL.js 的最大缺点是它不允许您以 SDL 编写模式,然后轻松生成 GraphQLSchema可执行版本。

如上所述,您可以使用 parsebuildASTSchema 从 SDL 创建 GraphQLSchema 实例,但这缺少使执行成为可能的必需的 resolve 函数!使您的 GraphQLSchema 可执行的唯一方法(使用 GraphQL.js)是手动将 resolve 函数添加到模式的字段。

graphql-tools 通过一个重要的功能弥补了这一差距:addResolveFunctionsToSchema。这非常有用,因为它可以用于为创建模式提供更友好的、基于 SDL 的 API。这正是 graphql-tools 使用 makeExecutableSchema 所做的事情

因此,使用 graphql-tools 的最大好处是它提供了将声明性模式与解析器连接起来的友好的 API!

何时不使用 graphql-tools

我们刚刚了解到,graphql-tools 的核心是在 GraphQL.js 之上提供了一个便利层,那么在某些情况下,它不是实现服务器的正确选择吗?

与大多数抽象一样,graphql-tools 通过在其他地方牺牲灵活性来使某些工作流程更容易。它提供了惊人的“入门”体验,并避免了快速构建 GraphQLSchema 时的摩擦。但是,如果您的后端有更多自定义要求,例如动态构建和修改您的模式,那么它的束缚可能会有点太紧——在这种情况下,您可以直接退回到使用 GraphQL.js。

关于 graphene-js 的简要说明

graphene-js 是一个新的 GraphQL 库,它遵循了其 Python 对等库的想法。它也在底层使用了 GraphQL.js,但不允许在 SDL 中进行模式声明。

graphene-js 深入拥抱现代 JavaScript 语法,提供了一个直观的 API,可以在其中将查询和变更实现为 JavaScript 类。看到更多 GraphQL 实现的出现,以用新鲜的想法丰富生态系统,这非常令人兴奋!

结论

在本文中,我们揭示了 GraphQL 执行引擎的机制和内部工作原理。从定义服务器 API 并确定将接受哪些查询和变更以及响应格式必须是什么样子的 GraphQL 模式开始。然后,我们深入研究了解析器函数,并概述了 GraphQL 引擎在解析传入查询时启用的执行模型。最后,概述了可用于帮助您实现 GraphQL 服务器的可用 JavaScript 库。

如果您想对本文中讨论的内容进行实践性的概述,请查看存储库。请注意,它具有 graphql-jsgraphql-tools 分支,以比较不同的方法。

总的来说,重要的是要注意 GraphQL.js 提供了构建 GraphQL 服务器所需的所有功能——graphql-tools 只是在其之上实现了一个便利层,该便利层满足了大多数用例,并提供了出色的“入门”体验。只有在构建 GraphQL 模式方面有更高级的要求时,才可能有意义摘下手套并使用纯 GraphQL.js。

下一篇文章中,我们将讨论网络层和用于实现 GraphQL 服务器的不同库,例如 express-graphqlapollo-servergraphql-yoga第 3 部分 然后介绍了 info 对象在 GraphQL 解析器中的结构和作用。

不要错过下一篇文章!

注册 Prisma 新闻通讯