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

初次接触 GraphQL 时,最先要问的问题之一就是 如何构建一个 GraphQL 服务器?由于 GraphQL 只是作为一项 规范 发布,您的 GraphQL 服务器实际上可以使用您偏好的任何编程语言来 实现。
在开始构建服务器之前,GraphQL 要求您设计一个 schema,它反过来定义了服务器的 API。在本文中,我们将了解 schema 的主要组件,阐明其实际实现的机制,并学习 GraphQL.js、graphql-tools
和 graphene-js
等库如何在此过程中为您提供帮助。
本文仅涉及 纯 GraphQL 功能——不涉及定义服务器与客户端通信 方式 的网络层概念。重点在于“GraphQL 执行引擎”的内部工作原理和查询解析过程。要了解网络层,请查看下一篇文章。
GraphQL schema 定义了服务器的 API
定义 schema:Schema 定义语言
GraphQL 有自己的类型语言,用于编写 GraphQL schema:Schema 定义语言 (SDL)。最简单的形式下,GraphQL SDL 可以用于定义如下所示的类型
单独的 User
类型不会向客户端应用程序公开任何功能,它只是定义了应用程序中用户 模型 的结构。为了向 API 添加功能,您需要将字段添加到 GraphQL schema 的根类型:Query
、Mutation
和 Subscription
。这些类型定义了 GraphQL API 的 入口点。
例如,考虑以下查询
只有当相应的 GraphQL schema 定义了具有以下 user
字段的 Query
根类型时,此查询才有效
因此,schema 的根类型决定了服务器将接受的查询和变更的形态。
GraphQL schema 为客户端-服务器通信提供了清晰的契约。
GraphQLSchema
对象是 GraphQL 服务器的核心
GraphQL.js 是 Facebook 的 GraphQL 参考实现,为 graphql-tools
和 graphene-js
等其他库提供了基础。使用这些库中的任何一个时,您的开发过程都围绕着一个 GraphQLSchema
对象展开,该对象由两个主要组件组成
- schema 定义
- 以 resolver 函数形式的实际 实现
对于上面的示例,GraphQLSchema
对象如下所示
如您所见,schema 的 SDL 版本可以直接转换为 GraphQLSchema
类型的 JavaScript 表示。请注意,此 schema 没有任何 resolver——因此它不会允许您实际 执行 任何查询或变更。下一节将详细介绍。
Resolver 实现 API
GraphQL 服务器中的结构与行为
GraphQL 明确分离了 结构 和 行为。GraphQL 服务器的 结构 ——正如我们刚刚讨论的——是其 schema,它是对服务器功能的一种抽象描述。这种结构通过具体的 实现 变得生动起来,该实现决定了服务器的 行为。实现的关键组件是所谓的 resolver 函数。
GraphQL schema 中的每个字段都由一个 resolver 支持。
最基本的形式是,GraphQL 服务器的 schema 中每个字段都有 一个 resolver 函数。每个 resolver 都知道如何为其字段获取数据。由于 GraphQL 查询的本质只是一组字段,因此 GraphQL 服务器要收集所需数据,只需调用查询中指定字段的所有 resolver 函数即可。(这也是为什么 GraphQL 经常与 RPC 风格的系统进行比较,因为它本质上是一种用于调用远程函数的语言。)
Resolver 函数解析
使用 GraphQL.js 时,GraphQLSchema
对象中类型的每个字段都可以附加一个 resolve
函数。让我们考虑上面的示例,特别是 Query
类型上的 user
字段——我们可以在这里添加一个简单的 resolve
函数,如下所示
假设 fetchUserById
函数实际可用并返回一个 User
实例(一个具有 id
和 name
字段的 JS 对象),那么 resolve
函数现在就可以启用 执行 的 schema。
在我们深入探讨之前,让我们花点时间了解一下传递给 resolver 的四个参数
root
(有时也称为parent
):还记得我们说过 GraphQL 服务器要解析查询所需要做的就是调用查询字段的 resolver 吗?它以 广度优先(逐层)的方式执行此操作,并且每个 resolver 调用中的root
参数只是上一个调用的结果(如果未另行指定,则初始值为null
)。args
:此参数承载查询的参数,在本例中是要获取的User
的id
。context
:一个在 resolver 链中传递的对象,每个 resolver 都可以对其进行读写(基本上是 resolver 之间进行通信和共享信息的一种方式)。info
:查询或变更的 AST 表示。您可以在本系列的第三部分中了解更多详细信息:GraphQL Resolver 中 info 参数的解密。
前面我们说过,GraphQL schema 中的每个字段都由一个 resolver 函数支持。目前我们只有一个 resolver,而我们的 schema 总共有三个字段:Query
类型上的根字段 user,以及 User
类型上的 id
和 name
字段。其余两个字段仍然需要它们的 resolver。正如您将看到的,这些 resolver 的实现非常简单
查询执行
考虑到我们上面的查询,让我们了解它是如何执行和收集数据的。该查询总共包含三个字段:user
(根字段)、id
和 name
。这意味着当查询到达服务器时,服务器需要调用三个 resolver 函数——每个字段一个。让我们逐步了解执行流程

- 查询到达服务器。
- 服务器调用根字段
user
的 resolver——我们假设fetchUserById
返回以下对象:{ "id": "abc", "name": "Sarah" }
- 服务器调用
User
类型上字段id
的 resolver。此 resolver 的root
输入参数是上一个调用的返回值,因此它可以简单地返回root.id
。 - 类似于 3,但最终返回
root.name
。(请注意,3 和 4 可以并行发生。) - 解析过程终止——最终结果将用
data
字段包装,以遵守 GraphQL 规范
现在,您真的需要自己为 user.id
和 user.name
编写 resolver 吗?使用 GraphQL.js 时,如果实现像示例中那样简单,则无需实现 resolver。因此,您可以省略它们的实现,因为 GraphQL.js 已经根据字段名称和 root 参数推断出需要返回的内容。
优化请求:DataLoader 模式
使用上述执行方法,当客户端发送深度嵌套的查询时,很容易遇到性能问题。假设我们的 API 也有带 评论 的 文章,并允许进行此查询
请注意,我们如何从给定 user
请求特定的 article
,以及其 comments
和撰写这些评论的用户的 name
。
假设这篇文章有五条评论,都由同一个用户撰写。这意味着我们将命中 writtenBy
resolver 五次,但每次都会返回相同的数据。DataLoader 允许您在这种情况下进行优化,以避免 N+1 查询问题——其普遍思想是 resolver 调用被批量处理,因此数据库(或其他数据源)只需被访问一次。
要了解更多关于 DataLoader 的信息,您可以观看 Lee Byron 的这部精彩视频:DataLoader — 源代码解析(约 35 分钟)
GraphQL.js 与 graphql-tools
现在我们来谈谈可用于在 JavaScript 中实现 GraphQL 服务器的库——主要是 GraphQL.js 和 graphql-tools
之间的区别。
GraphQL.js 为 graphql-tools 奠定了基础
首先要了解的关键一点是,GraphQL.js 为 graphql-tools
提供了基础。它通过定义所需的类型、实现 schema 构建以及查询验证和解析来完成所有繁重的工作。然后,graphql-tools
在 GraphQL.js 之上提供了一个轻薄的便利层。
让我们快速浏览一下 GraphQL.js 提供的函数。请注意,其功能通常围绕 GraphQLSchema
展开
parse
和buildASTSchema
:给定一个在 GraphQL SDL 中定义为 字符串 的 GraphQL schema,这两个函数将创建一个 GraphQLSchema 实例:const schema = buildASTSchema(parse(sdlString))
。validate
:给定一个GraphQLSchema
实例和一个查询,validate
确保该查询符合 schema 定义的 API。execute
:给定一个GraphQLSchema
实例和一个查询,execute
调用查询字段的 resolver,并根据 GraphQL 规范创建响应。当然,这只有当 resolver 是GraphQLSchema
实例的一部分时才有效(否则它就像一家只有菜单没有厨房的餐厅)。printSchema
:接受一个GraphQLSchema
实例,并以 SDL 形式(作为 字符串)返回其定义。
请注意,GraphQL.js 中最重要的函数是 graphql
,它接受一个 GraphQLSchema
实例和一个查询——然后调用 validate
和 execute
要了解所有这些函数,请查看这个使用它们进行简单示例的 Node 脚本。
graphql
函数正在对一个 schema 执行 GraphQL 查询,该 schema 本身已经包含 结构 和 行为。因此,graphql
的主要作用是编排 resolver 函数的调用,并根据所提供查询的形态打包响应数据。在这方面,graphql
函数实现的功能也称为 GraphQL 引擎。
graphql-tools
:连接接口与实现
使用 GraphQL 的好处之一是您可以采用 schema-first 开发流程,这意味着您构建的每个功能首先在 GraphQL schema 中体现——然后通过相应的 resolver 实现。这种方法有许多优点,例如它允许前端开发人员在后端开发人员实际实现 API 之前,就可以针对模拟 API 进行开发——这得益于 SDL。
GraphQL.js 最大的缺点是它不允许您以 SDL 编写 schema,然后轻松生成
GraphQLSchema
的 可执行 版本。
如上所述,您可以使用 parse
和 buildASTSchema
从 SDL 创建 GraphQLSchema
实例,但它缺少使执行成为可能所需的 resolve
函数!您使 GraphQLSchema
可执行(使用 GraphQL.js)的唯一方法是手动将 resolve
函数添加到 schema 的字段中。
graphql-tools
通过一个重要的功能弥补了这一空白:addResolveFunctionsToSchema
。这非常有用,因为它 T 可以用于提供一个更好、基于 SDL 的 API 来创建您的 schema。而这正是 graphql-tools
通过 makeExecutableSchema
所做的事情
因此,使用 graphql-tools
的最大好处是它提供了连接声明式 schema 与 resolver 的出色 API!
何时不使用 graphql-tools
?
我们刚刚了解到,graphql-tools
的核心是在 GraphQL.js 之上提供了一个便利层,那么有没有不适合用它来实现服务器的情况呢?
与大多数抽象一样,graphql-tools
通过牺牲其他地方的灵活性来简化某些工作流程。它提供了出色的“入门”体验,并避免了快速构建 GraphQLSchema
时的摩擦。但是,如果您的后端有更定制化的需求,例如动态构建和修改 schema,它的限制可能会有点太紧——在这种情况下,您可以退回使用纯 GraphQL.js。
关于 graphene-js
的简要说明
graphene-js
是一个遵循其Python 对应库思想的新 GraphQL 库。它也底层使用 GraphQL.js,但不允许在 SDL 中进行 schema 声明。
graphene-js
深度融合了现代 JavaScript 语法,提供了一个直观的 API,其中查询和变更可以作为 JavaScript 类来实现。看到更多 GraphQL 实现涌现出来,以新颖的理念丰富生态系统,这令人非常兴奋!
总结
在本文中,我们揭示了 GraphQL 执行引擎的机制和内部工作原理。首先介绍了定义服务器 API 并决定接受哪些查询和变更以及响应格式的 GraphQL schema。然后深入探讨了 resolver 函数,并概述了 GraphQL 引擎在解析传入查询时启用的执行模型。最后,总结了可用于实现 GraphQL 服务器的 JavaScript 库。
如果您想获得本文所讨论内容的实际概述,请查看此仓库。请注意,它有一个
graphql-js
和一个graphql-tools
分支,用于比较不同的方法。
总的来说,需要注意的是GraphQL.js 提供了构建 GraphQL 服务器所需的所有功能——而 graphql-tools
只是在其之上实现了一个便利层,满足了大多数用例并提供了出色的“入门”体验。只有在构建 GraphQL schema 方面有更高级要求时,才可能需要放开手脚,使用纯 GraphQL.js。
在下一篇文章中,我们将讨论网络层以及实现 GraphQL 服务器的不同库,如express-graphql、apollo-server 和graphql-yoga。第三部分将涵盖 GraphQL resolver 中 info 对象的结构和作用。
不要错过下一篇文章!
订阅 Prisma 新闻通讯