2017年12月6日

GraphQL 远程模式如何工作?

理解 GraphQL 模式拼接(第一部分)

How do GraphQL remote schemas work?

在本文中,我们希望了解如何使用任何现有 GraphQL API 并通过我们自己的服务器暴露它。在这种设置中,我们的服务器只是将接收到的 GraphQL 查询和变更转发到底层的 GraphQL API。负责转发这些操作的组件称为远程(可执行)模式

远程模式是一组被称为模式拼接的工具和技术的基础,这是 GraphQL 社区中的一个全新话题。在接下来的文章中,我们将更详细地讨论模式拼接的不同方法。

回顾:GraphQL 模式

上一篇文章中,我们已经介绍了 GraphQL 模式的基本机制和内部工作原理。让我们快速回顾一下!

在开始之前,重要的是要澄清术语GraphQL 模式,因为它可能有多种含义。在本文中,我们主要使用该术语来指代 GraphQLSchema 类的实例,该实例由 GraphQL.js 参考实现提供,并用作用 Node.js 编写的 GraphQL 服务器的基础。

模式由两个主要组件组成

  • 模式定义:这部分通常用 GraphQL 模式定义语言 (SDL) 编写,并以抽象方式描述 API 的能力,因此还没有实际的实现。本质上,模式定义指定了服务器将接受哪些类型的操作(查询、变更、订阅)。请注意,一个有效的模式定义需要包含 Query 类型——以及可选的 Mutation 和/或 Subscription 类型。(在代码中引用模式定义时,相应的变量通常称为 typeDefs。)
  • 解析器:这是模式定义变为现实并获得其实际行为的地方。解析器实现了模式定义指定的 API。(更多信息,请参阅上一篇文章。)

当一个模式既有模式定义又有解析器函数时,我们也称之为可执行模式。请注意,GraphQLSchema 的实例不一定可执行——它可能只包含模式定义而没有任何解析器附加。

这是一个简单的示例,使用了 graphql-tools 中的 makeExecutableSchema 函数

typeDefs 包含了模式定义,包括必需的 Query 和一个简单的 User 类型。resolvers 是一个对象,其中包含 Query 类型上定义的 user 字段的实现。

makeExecutableSchema 现在将模式定义中 SDL 类型中的字段映射到 resolvers 对象中定义的相应函数。它返回一个 GraphQLSchema 实例,我们现在可以使用它来执行实际的 GraphQL 查询,例如使用 GraphQL.js 中的 graphql 函数

因为 graphql 函数能够对 GraphQLSchema 的实例执行查询,所以它也被称为 *GraphQL(执行)引擎*。

GraphQL 执行引擎是一个程序(或函数),给定一个可执行模式和查询(或变更),它会产生一个有效的响应。因此,它的主要职责是协调可执行模式中解析器函数的调用,并根据 GraphQL 规范正确封装响应数据。

有了这些知识,让我们深入了解如何基于现有 GraphQL API 创建一个可执行的 GraphQLSchema 实例。

内省 GraphQL API

GraphQL API 的一个便利特性是它们允许内省。这意味着您可以通过发送所谓的*内省查询*来提取任何 GraphQL API 的*模式定义*。

考虑到上面的示例,您可以使用以下查询从模式中提取所有类型及其字段

这将返回以下 JSON 数据

如您所见,此 JSON 对象中的信息与我们上面基于 SDL 的模式定义等效(实际上并非 100% 等效,因为我们没有要求字段上的参数,但我们可以简单地扩展上面的内省查询以包含这些参数)。

创建远程模式

有了内省现有 GraphQL API 模式的能力,我们现在可以简单地创建一个新的 GraphQLSchema 实例,其模式定义与现有模式相同。这正是 graphql-toolsmakeRemoteExecutableSchema 的思想。

makeRemoteExecutableSchema 接收两个参数

  • 一个*模式定义*(您可以使用上面看到的内省查询获得)。请注意,最佳实践是在开发时就下载模式定义并将其作为 .graphql 文件上传到您的服务器,而不是在运行时发送内省查询(这会导致很大的性能开销)。
  • 一个连接到要代理的 GraphQL API 的 Link。本质上,这个 Link 是一个可以将查询和变更转发到现有 GraphQL API 的组件——所以它需要知道其(HTTP)端点。

从这里开始,makeRemoteExecutableSchema实现相当简单。模式定义被用作新模式的基础。但是解析器呢,它们从何而来?

显然,我们不能像下载模式定义那样*下载*解析器——解析器没有内省查询。但是,我们可以创建*新的*解析器,它们使用前面提到的 Link 组件,简单地将任何传入的查询或变更*转发*到底层 GraphQL API。

废话少说,让我们看一些代码!这里有一个基于 Graphcool CRUD API 的示例,用于名为 User 的类型,以创建一个远程模式,然后通过一个专用服务器(使用 graphql-yoga)暴露。

可以在这里找到此代码的运行示例

作为背景,User 类型的 CRUD API 看起来与此类似(完整版本可以在这里找到)

远程模式的幕后

让我们探讨上面示例中的 databaseServiceSchemaDefinitiondatabaseServiceExecutableSchema 在底层是什么样子。

检查 GraphQL 模式

首先要注意的是,它们都是 GraphQLSchema 的实例。然而,databaseServiceSchemaDefinition 只包含模式定义,而 databaseServiceExecutableSchema 实际上是一个可执行模式——这意味着它的类型字段上附加了解析器函数。

使用 Chrome 调试器,我们可以揭示 databaseServiceSchemaDefinition 是一个 JavaScript 对象,如下所示

GraphQLSchema 的一个非可执行实例GraphQLSchema 的一个非可执行实例

蓝色矩形显示了 Query 类型及其属性。正如预期,它有一个名为 allUsers 的字段(以及其他字段)。然而,在这个模式实例中,Query 的字段没有附加任何解析器——因此它不可执行。

让我们也看看 databaseServiceExecutableSchema

可执行模式 = 模式定义 + 解析器可执行模式 = 模式定义 + 解析器

这个截图看起来与我们刚刚看到的非常相似——除了 allUsers 字段现在附加了这个 resolve 函数。(Query 类型上的其他字段(Usernodeuser_allUsersMeta)也是如此,但在截图中不可见。)

我们可以更进一步,实际查看 resolve 函数的实现(请注意,此代码是由 makeRemoteExecutableSchema 动态生成的)

第 12-16 行是我们感兴趣的部分:一个名为 fetcher 的函数被调用,带有三个参数:queryvariablescontextfetcher 是根据我们之前提供的 Link 生成的,它基本上是一个能够将 GraphQL 操作发送到特定端点(用于创建 Link 的端点)的函数,这正是它在这里所做的。请注意,在第 13 行中作为查询值传递的实际 GraphQL 文档源自传递给解析器的 info 参数(参见第 10 行)。info 包含查询的 AST 表示。

非根解析器不进行网络调用

与我们上面探索 allUsers 根字段的解析器函数的方式相同,我们也可以调查 User 类型字段的解析器是什么样子。因此,我们需要导航到 databaseServiceExecutableSchema_typeMaps 属性中,在那里我们可以找到带有其字段的 User 类型

User 类型有两个字段:id 和 name(两者都有附加的解析器函数)User 类型有两个字段:id 和 name(两者都有附加的解析器函数)

两个字段(idname)都附加了一个 resolve 函数,这是由 makeRemoteExecutableSchema 生成的实现(请注意,这两个字段的实现是相同的)

有趣的是,这次生成的解析器没有使用 fetcher 函数——实际上它根本没有进行网络调用。返回的结果只是简单地从传递给函数的 parent 参数(第 10 行)中检索。

远程模式中的解析器追踪数据

远程可执行模式的解析器的追踪数据也证实了这一发现。在下面的截图中,我们用 ArticleComment 类型(每个都连接到 existingUser)扩展了之前的模式定义,以便我们可以发送更深层次的嵌套查询。

GraphQL Playgrounds 开箱即用地支持显示解析器的追踪数据(右下)GraphQL Playgrounds 开箱即用地支持显示解析器的追踪数据(右下)

从追踪数据中可以明显看出,只有根解析器(针对 allUsers 字段)耗时显著(167 毫秒)。所有负责返回非根字段数据的其余解析器仅需几微秒即可执行。这可以用我们之前观察到的现象来解释:根解析器使用 fetcher 转发接收到的查询,而所有非根解析器则根据传入的 parent 参数简单地返回它们的数据。

解析器策略

在实现模式定义的解析器函数时,有多种方法可以处理。

标准模式:类型级别解析

考虑以下模式定义

基于 Query 类型,可以向 API 发送以下查询

相应的解析器通常会如何实现?一个标准的方法如下(假设此代码中以 fetch 开头的函数正在从数据库加载资源)

通过这种方法,我们正在进行类型级别的解析。这意味着针对特定查询的实际对象(例如特定的 Article)是在 Article 类型的任何解析器被调用之前获取的。

考虑上面查询的解析器调用

  1. Query.user 解析器被调用,并从数据库加载一个特定的 User 对象。请注意,它将加载 User 对象的所有标量字段,包括 idname,尽管这些字段并未在查询中请求。但它尚未加载任何 articles 的内容——这将在下一步中发生。
  2. 接下来,User.articles 解析器被调用。请注意,输入参数 parent 是上一个解析器的返回值,因此它是一个完整的 User 对象,这允许解析器访问 Userid 来加载其对应的 Article 对象。

如果您在理解此示例时遇到困难,请务必阅读关于 GraphQL 模式的上一篇文章

远程可执行模式使用多级解析器方法

现在让我们再次思考远程模式示例及其解析器。我们了解到,当使用远程可执行模式执行查询时,数据源只会在根解析器中被命中*一次*(我们在那里找到了 fetcher——参见上面的截图)。所有其他解析器仅根据传入的 parent 参数返回规范结果(它是初始根解析器调用结果的一部分)。

但这如何工作呢?看起来根解析器在一个解析器中获取所有需要的数据——但这不是很低效吗?嗯,如果我们总是加载所有对象字段*包括*所有关系数据,那确实会非常低效。那么我们如何才能只加载传入查询中指定的数据呢?

这就是为什么远程可执行模式的根解析器会利用可用的 info 参数,该参数包含查询信息。通过查看实际查询的选择集,解析器无需加载对象的所有字段,而只加载它需要的字段。正是这个“技巧”使得在单个解析器中加载所有数据仍然高效。

总结

在本文中,我们学习了如何使用 graphql-tools 中的 makeRemoteExecutableSchema 为任何现有 GraphQL API 创建一个*代理*。这个代理被称为*远程可执行模式*,并运行在您自己的服务器上。它只是将接收到的任何查询转发到底层 GraphQL API。

我们还看到,这个远程可执行模式是使用*多级*解析器实现的,其中嵌套数据由第一个解析器一次性获取,而不是在类型级别多次获取。

关于远程模式,还有很多有待探索:这与模式拼接有什么关系?这与 GraphQL 订阅如何协同工作?我的 context 对象会怎样?在评论中告诉我们您接下来想学习什么!👋

不要错过下一篇文章!

订阅 Prisma 新闻通讯