为 Apollo Server 构建网络框架集成
Apollo Server 4背后的推动力之一是创建一个稳定、定义明确的API,用于处理HTTP请求和响应。Apollo Server4的API允许像您这样的外部合作伙伴在其选择的Web框架中构建与Apollo Server的集成。
概述
一个Apollo Server集成的首要责任是将Web框架的本地格式和用于Apollo Server的格式之间的请求和响应进行翻译。ApolloServer
。本文从概念上概述了如何构建集成,使用Express集成(即expressMiddleware
)作为例子。
如果您正在构建无服务器集成,我们强烈推荐您在函数名前加上单词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中,处理器接收req
和res
对象,并将它们传递到用户的上下文
函数。
如果用户没有提供上下文函数
,则一个空白的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部分(即method
、headers
和body
)以及其他有用的上下文信息。
请求处理程序有4个主要职责
- 解析请求
- 从传入请求中构造一个
HTTPGraphQLRequest
对象。 - 使用Apollo Server执行GraphQL请求。
- 向客户端返回一个格式良好的响应。
解析请求
Apollo Server可以响应各种请求,包括通过GET
和POST
方法,例如标准的GraphQL查询,APQs以及着陆页面请求(例如,Apollo Sandbox)。幸运的是,这些都属于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查询,其中包含query字符串参数。Apollo Server期望这些类型的HTTP请求有一个原始的查询字符串。Apollo Server对字符串开头是否包含?一视同仁。Fragments(以#开头)不应包含在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/serversearch: string;body: unknown;}
Apollo Server负责处理GET
与POST
、相关头信息以及是否在body或search中查找查询的GraphQL特定部分。因此,我们在HTTPGraphQLRequest
中有了method
、body
和search
属性。
最后,我们必须创建一个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传来的req
和res
对象传递给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();