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