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