2017 年 12 月 6 日

GraphQL 远程 Schema 是如何工作的?

理解 GraphQL schema stitching(第一部分)

How do GraphQL remote schemas work?

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

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

回顾:GraphQL Schema

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

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

Schema 由两个主要组件组成

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

当 schema 具有 schema 定义和解析器函数时,我们也将其称为可执行 schema。 请注意,GraphQLSchema的实例不一定是可执行的——它可能只包含 schema 定义,但没有任何附加的解析器。

以下是一个使用 graphql-tools 中的 makeExecutableSchema 函数的简单示例:

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

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

由于graphql函数能够针对GraphQLSchema的实例_执行_查询,因此它也被称为_GraphQL(执行)引擎_。

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

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

内省 GraphQL API

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

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

这将返回以下 JSON 数据:

正如你所看到的,此 JSON 对象中的信息等效于我们上面基于 SDL 的 schema 定义(实际上它不是 100% 等效的,因为我们没有请求字段上的_参数_,但我们可以简单地扩展上面的内省查询以包括这些参数)。

创建远程 schema

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

makeRemoteExecutableSchema接收两个参数:

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

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

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

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

在此处找到此代码的工作示例:此处

为了方便理解,User 类型的 CRUD API 看起来有点像这样(完整版本可以在这里找到)

深入了解远程 Schema

让我们研究一下上面例子中 databaseServiceSchemaDefinitiondatabaseServiceExecutableSchema 在底层是如何工作的。

检查 GraphQL Schema

首先要注意的是,它们都是 GraphQLSchema 的实例。但是,databaseServiceSchemaDefinition 仅包含 schema 定义,而 databaseServiceExecutableSchema 实际上是一个可执行的 schema —— 意味着它在其类型的字段上附加了解析函数。

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

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

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

让我们也看一下 databaseServiceExecutableSchema

可执行的 Schema = Schema 定义 + 解析器可执行的 Schema = Schema 定义 + 解析器

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

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

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

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

就像我们上面探索 allUsers 根字段的解析函数一样,我们也可以研究 User 类型字段的解析器是什么样的。因此,我们需要导航到 databaseServiceExecutableSchema_typeMaps 属性中,在那里我们可以找到具有其字段的 User 类型

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

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

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

在远程 Schema 中追踪解析器数据

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

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

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

解析器策略

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

标准模式:类型级别解析

考虑以下 schema 定义

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

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

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

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

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

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

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

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

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

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

总结

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

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

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

不要错过下一篇文章!

注册 Prisma 新闻通讯