在本文中,我们将了解如何使用任何现有的 GraphQL API,并通过我们自己的服务器将其公开。在这种设置中,我们的服务器只需将收到的 GraphQL 查询和变更转发到底层的 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 定义语言 (SDL) 编写,并以一种抽象的方式描述 API 的能力,因此还没有实际的实现。本质上,Schema 定义指定了服务器将接受哪些类型的操作(查询、变更、订阅)。请注意,有效的 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 和一个查询(或变更),它会产生一个有效的响应。因此,它的主要职责是协调可执行 Schema 中解析器函数的调用,并根据GraphQL 规范正确封装响应数据。
有了这些知识,让我们深入了解如何基于现有的 GraphQL API 创建一个可执行的GraphQLSchema实例。
自省 GraphQL API
GraphQL API 的一个方便的特性是它们允许进行自省。这意味着您可以通过发送所谓的自省查询来提取任何 GraphQL API 的Schema 定义。
考虑到上面的示例,您可以使用以下查询从 Schema 中提取所有类型及其字段
这将返回以下 JSON 数据
如您所见,此 JSON 对象中的信息等同于我们上面基于 SDL 的 Schema 定义(实际上它不是 100% 等同,因为我们没有询问字段上的参数,但我们可以简单地扩展上面的自省查询以包含这些参数)。
创建远程 Schema
有了自省现有 GraphQL API Schema 的能力,我们现在可以简单地创建一个新的GraphQLSchema实例,其 Schema 定义与现有实例相同。这正是graphql-tools的makeRemoteExecutableSchema的理念。

makeRemoteExecutableSchema接收两个参数
- 一个Schema 定义(您可以使用上面看到的自省查询获得)。请注意,最佳实践是在开发时下载 Schema 定义,并将其作为
.graphql文件上传到您的服务器,而不是在运行时发送自省查询(这会导致很大的性能开销)。 - 一个连接到要代理的 GraphQL API 的Link。本质上,这个 Link 是一个可以将查询和变更转发到现有 GraphQL API 的组件——所以它需要知道其(HTTP)端点。
makeRemoteExecutableSchema的实现从这里开始相当简单。Schema 定义用作新 Schema 的基础。但是解析器呢,它们从何而来?
显然,我们不能像下载 Schema 定义那样下载解析器——没有用于解析器的自省查询。但是,我们可以创建使用上述 Link 组件的新解析器,简单地将任何传入的查询或变更转发到底层 GraphQL API。
闲话少说,看代码吧!这是一个基于 Graphcool CRUD API 的示例,用于名为User的类型,以创建一个远程 Schema,然后通过专用服务器公开(使用graphql-yoga)
此代码的工作示例请见此处
就上下文而言,User 类型的 CRUD API 类似于这样(完整版本可以在此处找到)
远程 Schema 的底层原理
让我们研究一下上面示例中的databaseServiceSchemaDefinition和databaseServiceExecutableSchema在底层是什么样子。
检查 GraphQL Schema
首先要注意的是,它们都是GraphQLSchema的实例。然而,databaseServiceSchemaDefinition只包含 Schema 定义,而 databaseServiceExecutableSchema 实际上是一个可执行 Schema——这意味着它的类型字段上附加了解析器函数。
使用 Chrome 调试器,我们可以发现 databaseServiceSchemaDefinition 是一个 JavaScript 对象,如下所示
GraphQLSchema 的非可执行实例
蓝色矩形显示了Query类型及其属性。正如预期的那样,它有一个名为allUsers的字段(以及其他)。然而,在这个 Schema 实例中,Query的字段没有附加解析器——所以它不可执行。
我们再来看看databaseServiceExecutableSchema
可执行 Schema = Schema 定义 + 解析器
这个截图看起来与我们刚才看到的非常相似——不同之处在于allUsers字段现在附加了resolve函数。(对于Query类型上的其他字段(User、node、user和_allUsersMeta)也是如此,但未在截图中显示。)
我们可以更进一步,实际查看resolve函数的实现(请注意,此代码是由makeRemoteExecutableSchema动态生成的)
第 12-16 行是我们感兴趣的:一个名为fetcher的函数被调用,带有三个参数:query、variables和context。fetcher是基于我们之前提供的 Link 生成的,它基本上是一个能够将 GraphQL 操作发送到特定端点(用于创建 Link 的端点)的函数,这正是它在这里所做的。请注意,在第 13 行中作为查询值传递的实际 GraphQL 文档源自传递给解析器的 info 参数(参见第 10 行)。info包含查询的 AST 表示。
非根解析器不进行网络调用
以我们上面探索allUsers根字段的解析器函数相同的方式,我们也可以研究User类型上字段的解析器是什么样子的。因此,我们需要导航到databaseServiceExecutableSchema的_typeMaps属性,在那里我们可以找到User类型及其字段
User 类型有两个字段:id 和 name(两者都附加了一个解析器函数)
两个字段(id和name)都附加了一个resolve函数,这是由makeRemoteExecutableSchema生成的它们的实现(请注意,这两个字段的实现是相同的)
有趣的是,这次生成的解析器没有使用fetcher函数——实际上它根本没有进行网络调用。返回的结果只是简单地从传入函数的parent参数(第 10 行)中检索。
跟踪远程 Schema 中的解析器数据
远程可执行 Schema 的解析器跟踪数据也证实了这一发现。在下面的截图中,我们用Article和Comment类型(每个也都连接到existingUser)扩展了之前的 Schema 定义,以便我们可以发送一个更深层次的嵌套查询。
GraphQL Playgrounds支持开箱即用显示解析器的跟踪数据(右下角)
从跟踪数据中非常明显,只有根解析器(针对 allUsers 字段)花费了显著的时间(167 毫秒)。所有负责返回非根字段数据的其余解析器仅花费几微秒来执行。这可以用我们之前观察到的现象来解释,即根解析器使用fetcher来转发接收到的查询,而所有非根解析器则根据传入的parent参数简单地返回它们的数据。
解析器策略
在实现 Schema 定义的解析器函数时,有多种方法可以实现。
标准模式:类型级别解析
考虑以下 Schema 定义
根据Query类型,可以向 API 发送以下查询
相应的解析器通常是如何实现的?一个标准方法如下(假设此代码中以fetch开头的函数正在从数据库加载资源)
使用这种方法,我们正在进行类型级别的解析。这意味着特定查询的实际对象(例如特定Article)是在调用Article类型的任何解析器之前获取的。
考虑上述查询的解析器调用
Query.user解析器被调用并从数据库加载一个特定的User对象。请注意,它将加载User对象的所有标量字段,包括id和name,即使这些字段在查询中没有被请求。然而,它还没有为articles加载任何东西——这将在下一步发生。- 接下来,调用
User.articles解析器。请注意,输入参数parent是上一个解析器的返回值,因此它是一个完整的User对象,允许解析器访问User的id以加载其Article对象。
如果您在理解此示例时遇到困难,请务必阅读关于 GraphQL Schema 的上一篇文章。
远程可执行 Schema 使用多级解析器方法
现在让我们再次考虑远程 Schema 示例及其解析器。我们了解到,当使用远程可执行 Schema 执行查询时,数据源只会在根解析器中被命中一次(我们找到了fetcher——参见上面的截图)。所有其他解析器仅基于传入的parent参数返回规范结果(它是初始根解析器调用结果的子部分)。
但这如何运作?似乎根解析器在一个解析器中获取所有所需数据——但这难道不是超级低效吗?嗯,如果我们总是加载所有对象字段,包括所有关系数据,那确实会非常低效。那么我们如何才能只加载传入查询中指定的数据呢?
这就是为什么远程可执行 Schema 的根解析器利用可用的 info 参数,其中包含查询信息。通过查看实际查询的选择集,解析器无需加载对象的所有字段,而只需加载其所需的字段。这个“技巧”使得在单个解析器中加载所有数据仍然高效。
概要
在本文中,我们学习了如何使用graphql-tools中的makeRemoteExecutableSchema为任何现有 GraphQL API 创建一个代理。这个代理称为远程可执行 Schema,并在您自己的服务器上运行。它只是将收到的任何查询转发到底层 GraphQL API。
我们还看到,这个远程可执行 Schema 是使用多级解析器实现的,其中嵌套数据由第一个解析器一次性获取,而不是在类型级别多次获取。
关于远程 Schema 还有很多需要探索:这与 Schema Stitching 有何关系?它如何与 GraphQL 订阅协同工作?我的context对象会发生什么?在评论中告诉我们您接下来想学习什么!👋
不要错过下一篇文章!
订阅 Prisma 新闻通讯
