2017 年 11 月 14 日

GraphQL 服务器基础:GraphQL Schema、TypeDefs 和 Resolvers 解释

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

GraphQL Server Basics

刚开始接触 GraphQL 时,首先要问的问题之一是:我如何构建一个 GraphQL 服务器?由于 GraphQL 只是作为规范发布的,所以你的 GraphQL 服务器实际上可以用你喜欢的任何编程语言实现

在开始构建服务器之前,GraphQL 要求你设计一个schema,它反过来定义了服务器的 API。在这篇文章中,我们将了解 schema 的主要组件,阐明实际实现它的机制,并学习 GraphQL.jsgraphql-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 定义
  • 以解析器函数形式的实际实现

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

如你所见,schema 的 SDL 版本可以直接转换为 GraphQLSchema 类型的 JavaScript 表示。请注意,此 schema 没有解析器 — 因此它不允许你实际执行任何查询或变更。下一节将详细介绍。

解析器实现 API

GraphQL 服务器中的结构与行为

GraphQL 明确区分了结构行为。GraphQL 服务器的结构——正如我们刚刚讨论的——是它的 schema,对服务器功能的抽象描述。这种结构通过一个具体的实现变得生动起来,该实现决定了服务器的行为。实现的关键组件是所谓的解析器函数。

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

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

解析器函数剖析

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

假设函数 fetchUserById 确实可用并返回一个 User 实例(一个带有 idname 字段的 JS 对象),那么 resolve 函数现在就可以执行 schema 了。

在我们深入探讨之前,先花点时间理解传入解析器的四个参数:

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

之前我们说过,GraphQL schema 中的每个字段都由一个解析器函数支持。目前我们只有一个解析器,而我们的 schema 总共有三个字段:Query 类型上的根字段 user,以及 User 类型上的 idname 字段。剩下的两个字段仍然需要它们的解析器。如你所见,这些解析器的实现非常简单:

查询执行

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

Blog image
  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 已经根据字段名称和根参数推断出它需要返回什么。

优化请求: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 提供了基础。它通过定义所需的类型、实现 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 调用查询字段的解析器,并根据 GraphQL 规范创建响应。当然,这只有在解析器是 GraphQLSchema 实例的一部分时才有效(否则它只是一份有菜单但没有厨房的餐厅)。
  • printSchema:接受一个 GraphQLSchema 实例并以 SDL(作为字符串)返回其定义。

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

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

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

graphql-tools:连接接口与实现

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

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

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

graphql-tools 用一个重要的功能弥补了这一空白:addResolveFunctionsToSchema。这非常有用,因为它提供了一个更好的、基于 SDL 的 API 来创建你的 schema。这正是 graphql-toolsmakeExecutableSchema 所做的事情:

所以,使用 graphql-tools 的最大好处是它提供了一个很好的 API 来连接你的声明性 schema 和解析器!

何时不使用 graphql-tools

我们刚刚了解到 graphql-tools 的核心是提供了一个 GraphQL.js 之上的便利层,那么有没有不适合用它来实现服务器的情况呢?

与大多数抽象一样,graphql-tools 通过牺牲其他地方的灵活性来简化某些工作流。它提供了出色的“入门”体验,并在快速构建 GraphQLSchema 时避免了摩擦。但是,如果你的后端有更自定义的需求,例如动态构建和修改 schema,它的限制可能有点太紧——在这种情况下,你可以回退到使用 GraphQL.js。

关于 graphene-js 的简要说明

graphene-js 是一个新的 GraphQL 库,它遵循其Python 对应库的思想。它也在底层使用 GraphQL.js,但不允许在 SDL 中声明 schema。

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

结论

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

如果您想获得本文中讨论内容的实用概述,请查看存储库。请注意,它有一个 graphql-jsgraphql-tools 分支,用于比较不同的方法。

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

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

不要错过下一篇文章!

订阅 Prisma 新闻通讯

© . This site is unofficial and not affiliated with Prisma Data, Inc.