2017 年 12 月 06 日

GraphQL 远程模式如何工作?

理解 GraphQL schema stitching(第一部分)

How do GraphQL remote schemas work?

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

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

回顾:GraphQL 模式

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

在我们开始之前,重要的是消除术语 <强调>GraphQL 模式 的歧义,因为它可能意味着几件事。对于本文的上下文,我们将主要使用该术语来指代 GraphQLSchema 类的实例,该类由 GraphQL.js 参考实现提供,并用作 Node.js 中编写的 GraphQL 服务器的基础。

模式由两个主要组件组成

  • 模式定义:这部分通常用 GraphQL 模式定义语言 (SDL) 编写,并以<强调>抽象方式描述 API 的功能,因此还没有实际的<强调>实现。本质上,模式定义指定服务器将接受哪些类型的操作(查询、mutation、订阅)。请注意,为了使模式定义有效,它需要包含 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 执行引擎是一个程序(或函数),给定一个可执行模式和一个查询(或 mutation),它会生成有效的响应。因此,它的主要职责是协调可执行模式中解析器函数的调用,并正确打包响应数据,根据 GraphQL 规范

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

内省 GraphQL API

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

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

这将返回以下 JSON 数据

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

创建远程模式

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

makeRemoteExecutableSchema 接收两个参数

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

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

显然,我们不能像下载模式定义那样<强调>下载解析器 — 没有用于解析器的内省查询。但是,我们可以创建<强调>新的解析器,这些解析器使用提到的 Link 组件来简单地将任何传入的查询或 mutation <强调>转发到底层的 GraphQL API。

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

在此处查找此代码的工作示例 here

对于上下文,User 类型的 CRUD API 看起来有点类似于这样(完整版本可以在 here 中找到)

远程模式的幕后原理

让我们研究一下上面示例中的 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 行)中检索的。

在远程模式中跟踪解析器数据

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

GraphQL Playground 支持开箱即用地显示解析器的 tracing 数据(右下角)GraphQL Playground 支持开箱即用地显示解析器的 tracing 数据(右下角)

从 tracing 数据中可以非常明显地看出,只有根解析器(对于 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。

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

关于远程模式还有很多值得探索的地方:这与 schema stitching 有何关系?这如何与 GraphQL 订阅一起工作?我的 context 对象会发生什么?请在评论中告诉我们您接下来想了解什么!👋

不要错过下一篇文章!

注册 Prisma Newsletter