使用 Apollo Server 实现网关
使用 Node.js 网关作为您的图谱路由器
在设置至少一个联邦就绪子图后,您可以为您的图谱路由器(也称为网关)进行配置,使其位于您的子图之前。
📣 在大多数情况下,我们推荐使用 GraphOS 路由器 作为您的图谱路由器。 它配置更快,性能更优(特别是在高请求负载下),而且很少需要编写自定义代码。
在某些情况下,如果您子图使用的是当前难以与 GraphOS 路由器 配置的自定义身份验证方法,那么您可能需要使用 Apollo Server 作为您的图谱路由器。
无论您开始使用的是哪个图路库,您都可以在不需要对其他部分的 supergraph 进行任何更改的情况下切换到另一个。
Node.js 网关设置
本节将指导您如何使用 Apollo Server 和 @apollo/gateway
库设置基本的图谱路由器。它目前需要 Node.js 版本 14 或 16。
使用 npm init
创建一个新的 Node.js 项目,然后安装必要的包:
npm install @apollo/gateway @apollo/server graphql
@apollo/gateway@apollo/gateway
包包含 ApolloGateway 类。要配置 Apollo Server 以充当图谱路由器,您需要将一个 ApolloGateway 实例传递给 ApolloServer 构造函数,例如:
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { ApolloGateway } from '@apollo/gateway';import { readFileSync } from 'fs';const supergraphSdl = readFileSync('./supergraph.graphql').toString();// Initialize an ApolloGateway instance and pass it// the supergraph schema as a stringconst gateway = new ApolloGateway({supergraphSdl,});// Pass the ApolloGateway to the ApolloServer constructorconst server = new ApolloServer({gateway,});// Note the top-level `await`!const { url } = await startStandaloneServer(server);console.log(`🚀 Server ready at ${url}`);
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { ApolloGateway } from '@apollo/gateway';import { readFileSync } from 'fs';const supergraphSdl = readFileSync('./supergraph.graphql').toString();// Initialize an ApolloGateway instance and pass it// the supergraph schema as a stringconst gateway = new ApolloGateway({supergraphSdl,});// Pass the ApolloGateway to the ApolloServer constructorconst server = new ApolloServer({gateway,});// Note the top-level `await`!const { url } = await startStandaloneServer(server);console.log(`🚀 Server ready at ${url}`);
构建超级图模式
在上面的例子中,我们将supergraphSdl
选项传递给ApolloGateway
构造函数。这是我们的超级图模式的字符串表示形式,该模式由我们所有的子图模式组成。
要了解如何构建您的超级图模式,请参阅支持的方法。
在生产环境中,我们强烈建议使用Apollo Studio管理模式运行网关,这可以使您的网关无需重启即可更新其配置。有关详细信息,请参阅设置管理联邦。
启动时,网关处理您的supergraphSdl
,包括子图的路由信息。然后它开始接受传入请求并为它们创建查询计划,这些计划将跨一个或多个子图执行。
更新超级图模式
在上面的例子中,我们将一个静态超级图模式传递给网关。这种方法要求网关重启才能更新超级图模式。这对许多应用程序来说是不理想的,因此我们还提供了动态更新超级图模式的能力。
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { ApolloGateway } from '@apollo/gateway';import { readFile } from 'fs/promises';let supergraphUpdate;const gateway = new ApolloGateway({async supergraphSdl({ update }) {// `update` is a function that we'll save for later usesupergraphUpdate = update;return {supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'),};},});// Pass the ApolloGateway to the ApolloServer constructorconst server = new ApolloServer({gateway,});const { url } = await startStandaloneServer(server);console.log(`🚀 Server ready at ${url}`);
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { ApolloGateway } from '@apollo/gateway';import { readFile } from 'fs/promises';let supergraphUpdate;const gateway = new ApolloGateway({async supergraphSdl({ update }) {// `update` is a function that we'll save for later usesupergraphUpdate = update;return {supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'),};},});// Pass the ApolloGateway to the ApolloServer constructorconst server = new ApolloServer({gateway,});const { url } = await startStandaloneServer(server);console.log(`🚀 Server ready at ${url}`);
这里发生了一些事情。让我们逐个查看每件事情。
请注意,现在supergraphSdl
是一个async
函数。该函数在ApolloServer
初始化网关时恰好被调用一次。它有以下职责:
- 它接收了
update
函数,我们用它来更新 supergraph 模式。 - 它返回初始化的 supergraph 模式,这是网关启动时使用的。
使用 update
函数,我们现在可以以编程方式更新 supergraph 模式。轮询、webhook 和文件监视器都是我们可以采用的方法。
下面的代码演示了使用文件监视器的更完整示例。在这个例子中,假设我们正在使用 Rover CLI
(矢量 CLI) 更新 supergraphSdl.graphql
文件。
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { ApolloGateway } from '@apollo/gateway';import { watch } from 'fs';import { readFile } from 'fs/promises';const server = new ApolloServer({gateway: new ApolloGateway({async supergraphSdl({ update, healthCheck }) {// create a file watcherconst watcher = watch('./supergraph.graphql');// subscribe to file changeswatcher.on('change', async () => {// update the supergraph schematry {const updatedSupergraph = await readFile('./supergraph.graphql', 'utf-8');// optional health check update to ensure our services are responsiveawait healthCheck(updatedSupergraph);// update the supergraph schemaupdate(updatedSupergraph);} catch (e) {// handle errors that occur during health check or while updating the supergraph schemaconsole.error(e);}});return {supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'),// cleanup is called when the gateway is stoppedasync cleanup() {watcher.close();},};},}),});const { url } = await startStandaloneServer(server);console.log(`🚀 Server ready at ${url}`);
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import { ApolloGateway } from '@apollo/gateway';import { watch } from 'fs';import { readFile } from 'fs/promises';const server = new ApolloServer({gateway: new ApolloGateway({async supergraphSdl({ update, healthCheck }) {// create a file watcherconst watcher = watch('./supergraph.graphql');// subscribe to file changeswatcher.on('change', async () => {// update the supergraph schematry {const updatedSupergraph = await readFile('./supergraph.graphql', 'utf-8');// optional health check update to ensure our services are responsiveawait healthCheck(updatedSupergraph);// update the supergraph schemaupdate(updatedSupergraph);} catch (e) {// handle errors that occur during health check or while updating the supergraph schemaconsole.error(e);}});return {supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'),// cleanup is called when the gateway is stoppedasync cleanup() {watcher.close();},};},}),});const { url } = await startStandaloneServer(server);console.log(`🚀 Server ready at ${url}`);
这个例子更完整一些。让我们看看我们增加了哪些内容。
在 supergraphSdl
回调中,我们还接收了一个 healthCheck
函数。这使我们能够对未来的 supergraph 模式中的每个服务进行健康检查。这有助于确保我们的服务响应良好,并且我们在不安全时不会执行更新。
我们还使用了 try
块包装了我们的 update
和 healthCheck
调用。如果在其中任何一个过程中发生错误,我们希望优雅地处理它。在这个例子中,我们继续运行现有的 supergraph 模式并记录错误。
最后,我们返回一个 cleanup
函数。这是一个在网关停止时被调用的回调。这使我们能够在通过 ApolloServer.stop
调用关闭网关时,干净地关闭任何正在运行的过程(如文件监视器或轮询)。网关期望 cleanup
返回一个 Promise
,在关闭之前对其进行 await
。
高级用法
在一个更复杂的应用程序中,您可能希望创建一个处理 update
和 healthCheck
函数以及任何其他状态的类。在这种情况下,您可以选择提供带有 initialize
函数的对象(或类)。此函数就像上面讨论的 supergraphSdl
函数那样被调用。有关此示例,请参阅 IntrospectAndCompose
源代码。
使用 IntrospectAndCompose
组合子图
⚠️ 我们强烈反对在生产环境中使用 IntrospectAndCompose
。有关详细信息,请参阅IntrospectAndCompose
的限制。
您可以直接通过将IntrospectAndCompose
类的实例与subgraphs
数组提供来指示网关获取所有的subgraph模式并自己执行组合。要这样做,请提供如下所示的IntrospectAndCompose
类实例:
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');const gateway = new ApolloGateway({supergraphSdl: new IntrospectAndCompose({subgraphs: [{ name: 'accounts', url: 'https://127.0.0.1:4001' },{ name: 'products', url: 'https://127.0.0.1:4002' },// ...additional subgraphs...],}),});
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');const gateway = new ApolloGateway({supergraphSdl: new IntrospectAndCompose({subgraphs: [{ name: 'accounts', url: 'https://127.0.0.1:4001' },{ name: 'products', url: 'https://127.0.0.1:4002' },// ...additional subgraphs...],}),});
在subgraphs
数组中的每个项目都是一个对象,指定了您的子图的名称和url
。您可以指定任何字符串作为name
的值,它主要用查询规划器输出、错误消息和日志。
启动时,网关将从其url
获取每个子图的模式,并将这些模式组合成一个supergraph模式。然后,它开始接受传入的请求并为这些请求创建查询计划,在单个或多个子图上执行。
更多配置选项,请参阅IntrospectAndCompose
API 文档。
然而,IntrospectAndCompose
有一些重要的限制。
IntrospectAndCompose 限制
IntrospectAndCompose
有时可能对本地开发有帮助,但对于任何其他环境都强烈反对。以下是一些原因:
- 组合可能会失败。在使用
IntrospectAndCompose
的情况下,网关在启动时动态执行组合,这需要与每个子图进行网络通信。如果组合失败,您的网关将引发错误并经历非计划的停机时间。- 使用静态或动态
supergraphSdl
配置时,您需要提供一个已经成功组合的 supergraph schema。这样可以防止组合错误并加快启动速度。
- 使用静态或动态
- 网关实例可能不同。如果在部署子图更新时部署多个您的网关实例,网关实例可能会从同一个子图获取不同的 schemas。这可能导致实例间的组合失败或不一致的 supergraph schemas。
- 当使用
supergraphSdl
部署多个实例时,您为每个实例提供了完全相同的静态工件,这可以使行为更加可预测。
- 当使用
更新网关
在更新网关版本之前,检查 变更日志以查找潜在的破坏性更改。
我们强烈建议在部署到预演或生产环境之前,在本地和测试环境中更新您的网关。
您可以使用 npm list
命令来确认 @apollo/gateway
库的当前安装版本:
npm list @apollo/gateway
要更新库,请使用 npm update
命令:
npm update @apollo/gateway
这将更新库到允许的最新版本。 了解更多关于依赖约束的信息。
要将版本更新到特定版本(包括超过你的依赖性约束的版本),请使用npm install
代替:
自定义请求和响应
网关可以在执行跨你的子图执行之前修改入站请求的详细信息。例如,你的子图可能都使用相同的授权令牌将入站请求与特定用户相关联。网关可以将该令牌添加到它发送到子图的每个操作。
同样,网关可以根据每个子图返回的结果修改其对客户端的响应的详细信息。
自定义请求
在以下示例中,每个到达网关的请求都包含一个Authorization
头。网关通过读取该头并使用它来查找关联的用户ID,为操作设置共享的contextValue
。
在将userId
添加到共享的contextValue
对象之后,网关可以将该值添加到它包含在每个子图请求中的头中。
如果你使用不同的Apollo Server 集成,则传递给你的context
函数的对象字段可能不同。
构造函数buildService
使我们能够自定义发送到子图的请求。在此示例中,我们返回一个自定义的RemoteGraphQLDataSource
。此数据源使我们能够在发送之前使用来自 Apollo Server 的contextValue
信息修改出站请求。在此处,我们将user-id
头传递给下游服务,以传递已验证的用户ID。
自定义响应
假设每当子图将其操作结果返回给网关时,它在响应中包括一个Server-Id
头。该头的值独特地标识了我们的图中子图。
然后当网关响应客户端时,我们希望它的其 Server-Id
头包含对响应有贡献的每个子图的标识符。在这种情况下,我们可以让网关将各种服务器ID聚合为单个、以逗号分隔的列表。
从客户端应用程序处理单个操作的流程如下:
为了实现此流程,我们可以使用didReceiveResponse
回调函数的RemoteGraphQLDataSource
类来检查子图的结果,作为它们到达。我们可以在该回调中添加Server-Id
到共享的context
中,然后在向客户端发送最终响应时从context
中获取完整的列表。
在此示例中,多次调用 didReceiveResponse
将值推送到共享的 contextValue.serverIds
数组。无法保证这些调用的顺序。如果您编写修改共享 contextValue
对象的逻辑,请确保修改不会造成破坏,并且修改的顺序不需要保证。
有关 buildService
和 RemoteGraphQLDataSource
的更多信息,请参阅 API 文档。
自定义指令支持
@apollo/gateway
库支持在您的子图模式中使用自定义 指令。这种支持根据给定的 指令 是类型系统指令还是可执行指令而有所不同。
类型系统指令
类型系统 指令是应用于以下 位置 中的指令。这些 directives 不会被用于操作内部,而是在架构本身的这些位置应用。
下面的 @deprecated
指令是一个类型系统指令的例子:
directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUEtype ExampleType {newField: StringoldField: String @deprecated(reason: "Use `newField`.")}
网关将剥离所有定义和使用类型系统指令从您的图's API架构。这在您的子图架构中没有任何影响,它们保留这些信息。
实际上,网关通过忽略支持类型系统指令,使其成为定义它们的子图的责任。
可执行指令
可执行指令是指应用于以下位置之一这些位置的指令。这些指令在您的架构中定义,但它们被用于客户端发送的操作。
以下是一个可执行指令定义的示例:
# Uppercase this field's value (assuming it's a string)directive @uppercase on FIELD
以下是一个使用该指令的查询示例:
query GetUppercaseUsernames {users {name @uppercase}}
强烈建议所有子图都为给定的可执行指令使用完全相同的逻辑。否则,操作可能会产生对客户端不一致或令人困惑的结果。