在本文中,我们希望理解如何使用<强调>任何强调>现有的 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 中找到)
远程模式的幕后原理
让我们研究一下上面示例中的 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 行)中检索的。
在远程模式中跟踪解析器数据
远程可执行模式的解析器的 tracing 数据也证实了这一发现。在以下屏幕截图中,我们使用 Article
和 Comment
类型扩展了之前的模式定义(每个类型也连接到 existingUser
),以便我们可以发送更深层嵌套的查询。
GraphQL Playground 支持开箱即用地显示解析器的 tracing 数据(右下角)
从 tracing 数据中可以非常明显地看出,只有根解析器(对于 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。
我们还看到,此远程可执行模式是使用<强调>多级强调>解析器实现的,其中嵌套数据由第一个解析器单次获取,而不是在类型级别上多次获取。
关于远程模式还有很多值得探索的地方:这与 schema stitching 有何关系?这如何与 GraphQL 订阅一起工作?我的 context
对象会发生什么?请在评论中告诉我们您接下来想了解什么!👋
不要错过下一篇文章!
注册 Prisma Newsletter