加入我们,于10月8日至10日在纽约市了解关于GraphQL联盟和API平台工程的最新技巧、趋势和新闻。加入我们,参加2024年纽约市GraphQL峰会
文档
免费开始

为 Apollo Server 构建网络框架集成


本文面向框架集成的作者。在构建新的集成之前,我们建议先查看是否有适合您框架的集成以满足您的需求。

Apollo Server 4背后的推动力之一是创建一个稳定、定义明确的API,用于处理HTTP请求和响应。4的API允许像您这样的外部合作伙伴在其选择的Web框架中构建与Apollo Server的集成。

概述

一个Apollo Server集成的首要责任是将Web框架的本地格式和用于Apollo Server的格式之间的请求和响应进行翻译。ApolloServer。本文从概念上概述了如何构建集成,使用Express集成(即expressMiddleware)作为例子。

有关更多示例,请参阅这些Apollo Server 4Fastify和Lambda集成演示

如果您正在构建集成,我们强烈推荐您在函数名前加上单词start(例如startServerAndCreateLambdaHandler(server))。这种命名约定有助于保持Apollo Server的标准,即每个服务器都使用一个包含单词start(例如startStandaloneServer(server))的函数或方法。

主函数签名

让我们从查看主函数签名开始。下面的示例片段使用函数重载来为ApolloServer实例和用户的context函数

以下是两个expressMiddleware定义的允许签名,而第三个是实际实现:

interface ExpressMiddlewareOptions<TContext extends BaseContext> {
context?: ContextFunction<[ExpressContextFunctionArgument], TContext>;
}
export function expressMiddleware(
server: ApolloServer<BaseContext>,
options?: ExpressMiddlewareOptions<BaseContext>,
): express.RequestHandler;
export function expressMiddleware<TContext extends BaseContext>(
server: ApolloServer<TContext>,
options: WithRequired<ExpressMiddlewareOptions<TContext>, 'context'>,
): express.RequestHandler;
export function expressMiddleware<TContext extends BaseContext>(
server: ApolloServer<TContext>,
options?: ExpressMiddlewareOptions<TContext>,
): express.RequestHandler {
// implementation details
}

在第一个 expressMiddleware 签名中,如果用户没有提供 options,则不存在要调用的用户提供的 context 函数。生成的 context 对象是一个 BaseContext(或 {{)。

第二个 expressMiddleware 签名 需要 options 接收一个 context 属性。这意味着 Apollo Server 期望 context 对象的类型与用户提供的 context 函数的类型相同。Apollo Server 使用 TContext 类型来表示 GraphQL 上下文对象的类型。在上面,ApolloServer 实例和用户提供的 context 函数共享同一个 TContext 泛型,确保用户正确地为他们的服务器和 context 函数进行类型注解。

确保成功启动

对于标准的集成,用户应在将他们的服务器实例传递给集成之前,等待 server.start()。这样可以确保服务器正确启动,并且允许您的集成用户处理任何启动错误。

要保证服务器已启动,可以使用 Apollo Server 上的 assertStarted 方法的如下方式:

server.assertStarted('expressMiddleware()');

无服务器 集成不需要用户调用 server.start();相反,无服务器集成调用 startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests 方法。因为无服务器集成处理启动它们的服务器实例,所以它们也不需要调用 assertStarted 方法。

计算 GraphQL 上下文

请求处理程序可以访问关于传入请求的各类信息,这在GraphQL执行过程中非常有用。集成功能应该提供钩子供用户使用,以便他们能够根据传入请求的值创建自己的GraphQL上下文对象

如果用户提供了一个上下文函数,则它应该接收请求对象和处理器接收的任何其他上下文信息。例如,在Express中,处理器接收reqres对象,并将它们传递到用户的上下文函数。

如果用户没有提供上下文函数,则一个空白的GraphQL上下文对象就足够了(请参见下面的defaultContext)。

Apollo Server导出了一个通用的ContextFunction类型,这对于定义API的集成来说非常有用。上面,expressMiddleware函数签名使用了ContextFunction类型在ExpressMiddlewareOptions接口中,为用户提供了一个带正确参数类型的强类型上下文函数。

ContextFunction类型的第一指定了一个集成需要将其传递到用户上下文函数中的哪些。第二个变量定义了用户上下文函数的返回类型,它应该使用与ApolloServer相同的TContext通用类型:

interface ExpressContextFunctionArgument {
req: express.Request;
res: express.Response;
}
const defaultContext: ContextFunction<
[ExpressContextFunctionArgument],
any
> = async () => ({});
const context: ContextFunction<[ExpressContextFunctionArgument], TContext> =
options?.context ?? defaultContext;

请注意,上下文函数执行步骤期间被调用。

处理请求

我们建议将您的集成包实现为一个请求处理程序或框架插件。请求处理程序通常接收有关每个请求的信息,包括标准HTTP部分(即methodheadersbody)以及其他有用的上下文信息。

请求处理程序有4个主要职责

  1. 解析请求
  2. 从传入请求中构造一个HTTPGraphQLRequest对象
  3. 使用Apollo Server执行GraphQL请求
  4. 向客户端返回一个格式良好的响应

解析请求

Apollo Server可以响应各种请求,包括通过GETPOST方法,例如标准的GraphQL查询,以及着陆页面请求(例如,)。幸运的是,这些都属于Apollo Server的核心逻辑,因此集成开发者无需担心。

集成负责解析请求体,并使用这些值构建Apollo Server期望的HTTPGraphQLRequest

在Apollo Server 4的Express集成中,用户需要设置body-parser JSON中间件,该中间件用于解析带有application/json类型content-type的JSON请求体。集成可以为它们的生态系统需要类似的中间件(或插件),或者它们可以自行处理请求体的解析。

例如,正确解析的身体应该具有类似于以下的结构:

{
query?: string;
variables?: Record<string, any>;
operationName?: string;
extensions?: Record<string, any>;
}

您的集成应该将解析出的内容传递给Apollo Server;Apollo Server将处理验证解析后的请求。

Apollo Server也接受通过使用GET方法发送的GraphQL查询,其中包含字符串参数。Apollo Server期望这些类型的HTTP请求有一个原始的查询字符串。Apollo Server对字符串开头是否包含?一视同仁。(以#开头)不应包含在URL的末尾。

Apollo Server 4的Express集成使用请求的完整URL来计算查询字符串,如下所示:

import { parse } from 'url';
const search = parse(req.url).search ?? '';

构造HTTPGraphQLRequest对象

在请求体解析完成后,我们现在可以构建一个HTTPGraphQLRequest

interface HTTPGraphQLRequest {
method: string;
headers: HeaderMap; // the `HeaderMap` class is exported by @apollo/server
search: string;
body: unknown;
}

Apollo Server负责处理GETPOST、相关头信息以及是否在bodysearch中查找查询的GraphQL特定部分。因此,我们在HTTPGraphQLRequest中有了methodbodysearch属性。

最后,我们必须创建一个headers属性,因为Apollo Server期望头信息是一个Map

在Express集成中,我们通过迭代headers对象来构造一个Map,如下所示:

import { HeaderMap } from '@apollo/server';
const headers = new HeaderMap();
for (const [key, value] of Object.entries(req.headers)) {
if (value !== undefined) {
headers.set(key, Array.isArray(value) ? value.join(', ') : value);
}
}

Apollo Server期望头键是唯一的小写。如果你的框架允许重复的键,你需要将这些额外键的值合并为单个键,之间用, 连接(如上所示)。

在上述代码片段中,Express已经提供了小写的头键,因此这种方法可能不适合您的框架。

我们现在已经拥有了所有 HTTPGraphQLRequest 的组件,可以构建对象,如下所示:

const httpGraphQLRequest: HTTPGraphQLRequest = {
method: req.method.toUpperCase(),
headers,
body: req.body,
search: parse(req.url).search ?? '',
};

执行GraphQL请求

使用我们上面创建的 HTTPGraphQLRequest,我们现在执行GraphQL请求:

const result = await server
.executeHTTPGraphQLRequest({
httpGraphQLRequest,
context: () => context({ req, res }),
});

在上面的代码片段中,变量 httpGraphQLRequest是我们的 HTTPGraphQLRequest 对象。函数 context 是我们之前确定的一个(要么是由用户提供的,要么是我们的默认上下文)。注意,我们如何将Express传来的reqres对象传递给context函数(正如我们的ExpressContextFunctionArgument类型所承诺的那样)。

处理错误

executeHTTPGraphQLRequest 方法不会抛出异常。相反,它返回一个包含有用错误和特定 status 的对象(如果适用)。您应根据适用于您框架的错误处理约定相应地处理此对象。

在Express集成中,这不需要任何特殊处理。非错误情况设置状态码和头部,然后与错误情况一样响应执行结果。

发送响应

等待executeHTTPGraphQLRequest返回的Promise后,我们收到一个 HTTPGraphQLResponse 类型的对象。此时,根据您框架的约定,您的处理程序应根据此响应向客户端做出响应。

interface HTTPGraphQLHead {
status?: number;
headers: HeaderMap;
}
type HTTPGraphQLResponseBody =
| { kind: 'complete'; string: string }
| { kind: 'chunked'; asyncIterator: AsyncIterableIterator<string> };
type HTTPGraphQLResponse = HTTPGraphQLHead & {
body: HTTPGraphQLResponseBody;
};

请注意,正文可以是“完整”(可以立即发送的完整响应,带有content-length头部),或者“分块”,在这种情况下,集成应从异步迭代器读取并发送每个块。这通常使用transfer-encoding: chunked,尽管您的Web框架可能自动处理这一点。如果您的Web环境不支持流式响应(例如在某些无服务器函数环境中如AWS Lambda),则在接收到分块正文时可以返回错误响应。

Express实现使用res对象更新响应的状态码和头部,然后发送正文。请注意,在Express中,res.send将发送完整的正文(包括计算content-length头部),而res.write将使用transfer-encoding: chunked。Express没有内置的“flush”方法,但流行的compression中间件(支持accept-encoding: gzip类似头部)向响应中添加了一个flush方法;因为响应压缩通常在达到一定块大小时缓冲输出,所以您应确保您的集成与您的Web框架的响应压缩功能兼容。

for (const [key, value] of httpGraphQLResponse.headers) {
res.setHeader(key, value);
}
res.statusCode = httpGraphQLResponse.status || 200;
if (httpGraphQLResponse.body.kind === 'complete') {
res.send(httpGraphQLResponse.body.string);
return;
}
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
res.write(chunk);
if (typeof (res as any).flush === 'function') {
(res as any).flush();
}
}
res.end();

附加资源

上一个
集成
下一个
MERN 堆栈教程
评价文章评价

©2024Apollo Graph Inc.,即 Apollo GraphQL。

隐私政策

公司