2017年11月14日

GraphQL 服务器基础:GraphQL Schema、TypeDefs 和 Resolver 详解

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

GraphQL Server Basics

初次接触 GraphQL 时,最先要问的问题之一就是 如何构建一个 GraphQL 服务器?由于 GraphQL 只是作为一项 规范 发布,您的 GraphQL 服务器实际上可以使用您偏好的任何编程语言来 实现

在开始构建服务器之前,GraphQL 要求您设计一个 schema,它反过来定义了服务器的 API。在本文中,我们将了解 schema 的主要组件,阐明其实际实现的机制,并学习 GraphQL.js、graphql-toolsgraphene-js 等库如何在此过程中为您提供帮助。

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

GraphQL schema 定义了服务器的 API

定义 schema:Schema 定义语言

GraphQL 有自己的类型语言,用于编写 GraphQL schema:Schema 定义语言 (SDL)。最简单的形式下,GraphQL SDL 可以用于定义如下所示的类型

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

例如,考虑以下查询

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

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

GraphQL schema 为客户端-服务器通信提供了清晰的契约。

GraphQLSchema 对象是 GraphQL 服务器的核心

GraphQL.js 是 Facebook 的 GraphQL 参考实现,为 graphql-toolsgraphene-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 实例(一个具有 idname 字段的 JS 对象),那么 resolve 函数现在就可以启用 执行 的 schema。

在我们深入探讨之前,让我们花点时间了解一下传递给 resolver 的四个参数

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

前面我们说过,GraphQL schema 中的每个字段都由一个 resolver 函数支持。目前我们只有一个 resolver,而我们的 schema 总共有三个字段:Query 类型上的根字段 user,以及 User 类型上的 idname 字段。其余两个字段仍然需要它们的 resolver。正如您将看到的,这些 resolver 的实现非常简单

查询执行

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

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

现在,您真的需要自己为 user.iduser.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 展开

  • parsebuildASTSchema:给定一个在 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 实例和一个查询——然后调用 validateexecute

要了解所有这些函数,请查看这个使用它们进行简单示例的 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可执行 版本。

如上所述,您可以使用 parsebuildASTSchema 从 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-graphqlapollo-servergraphql-yoga。第三部分将涵盖 GraphQL resolver 中 info 对象的结构和作用。

不要错过下一篇文章!

订阅 Prisma 新闻通讯

© . All rights reserved.