Skip to content
Linbudu's Blog

Apollo-Server-Vercel 源码浅析

1 min read

总共就四个文件:

  • index.ts
  • ApolloServer.ts
  • setHeaders.ts
  • vercelApollo.ts

Index.ts

1export { ApolloServer } from "./ApolloServer";
2export type { CreateHandlerOptions } from "./ApolloServer";

入口文件, 不做讲解

ApolloServer.ts

先去掉了文件上传相关的代码实现

继承了apollo-server中导出的ApolloServerBase类, 然后实现了createGraphQLServerOptionscreateHandler方法, 第一个:

1createGraphQLServerOptions(req: NowRequest, res: NowResponse): Promise<GraphQLOptions> {
2 return super.graphQLServerOptions({ req, res });
3 }

暂时没搞懂这是用来干啥的, 全局只有这个 文件上传相关的, 暂时跳过.

graphQLServerOptions这个方法的作用是从对象中生成graphql options, 对象包括请求(http.IncomingRequests)以及特定实现(Express/Koa/Hapi/...)的选项, 这里的选项应该是指像Apollo-Server-Koa中传入的选项那样, 因为这个方法定义在Apollo-Server-Core里面.

createHandler, 绝大部分是在处理 cors...

首先生成一个空的 Header 类:

1// Headers 类来自于node-fetch
2const corsHeaders = new Headers();

然后判断创建句柄(Handler)时是否传入了cors选项:

1if (cors) {
2 if (cors.methods) {
3 // 设置 access-control-allow-methods字段
4 }
5
6 if (cors.allowedHeaders) {
7 // 设置 access-control-allow-headers 字段
8 }
9
10 if (cors.exposedHeaders) {
11 // 设置 access-control-expose-headers 字段
12 }
13
14 if (cors.credentials) {
15 corsHeaders.set(`access-control-allow-credentials`, `true`);
16 }
17 if (typeof cors.maxAge === `number`) {
18 corsHeaders.set(`access-control-max-age`, cors.maxAge.toString());
19 }
20}

其实这里有个思路, 比如我实现一个Apollo-Server-Koa-Vercel, 那是不是可以用中间件的形式来配置 cors 就好了.

然后返回一个异步函数, 即Vercel Functions的规范:

1return async (req: NowRequest, res: NowResponse) => {
2 // ...
3};

内部:

首先使用上面的 cors 头字段来配置请求头:

1const requestCorsHeaders = new Headers(corsHeaders);

cors.origin做处理:

1if (cors && cors.origin) {
2 const requestOrigin = req.headers.origin;
3 if (typeof cors.origin === `string`) {
4 requestCorsHeaders.set(`access-control-allow-origin`, cors.origin);
5 } else if (
6 requestOrigin &&
7 (typeof cors.origin === `boolean` ||
8 (Array.isArray(cors.origin) &&
9 requestOrigin &&
10 cors.origin.includes(requestOrigin as string)))
11 ) {
12 requestCorsHeaders.set(
13 `access-control-allow-origin`,
14 requestOrigin as string
15 );
16 }
17
18 const requestAccessControlRequestHeaders =
19 req.headers[`access-control-request-headers`];
20 if (!cors.allowedHeaders && requestAccessControlRequestHeaders) {
21 requestCorsHeaders.set(
22 `access-control-allow-headers`,
23 requestAccessControlRequestHeaders as string
24 );
25 }
26}

然后将其拼装为对象:

1const requestCorsHeadersObject = Array.from(requestCorsHeaders).reduce<
2 Record<string, string>
3>((headersObject, [key, value]) => {
4 headersObject[key] = value;
5 return headersObject;
6}, {});

这一部分我的理解是对请求做处理, 这样在前端跨域调用 faas 的时候就不会被拦截了.

然后快速通过 OPTIONS 请求:

1if (req.method === `OPTIONS`) {
2 setHeaders(res, requestCorsHeadersObject);
3 return res.status(204).send(``);
4}

下面还对req.url = '/.well-known/apollo/server-health'这种情况做了处理, 是 Apollo 的 onHealthCheck 相关的, 也跳过.

在最后一部分是对graphql-playground的处理:

1if (this.playgroundOptions && req.method === `GET`) {
2 const acceptHeader = req.headers.Accept || req.headers.accept;
3 if (acceptHeader && acceptHeader.includes(`text/html`)) {
4 const path = req.url || `/`;
5 const playgroundRenderPageOptions: PlaygroundRenderPageOptions = {
6 endpoint: path,
7 ...this.playgroundOptions,
8 };
9
10 setHeaders(res, {
11 "Content-Type": `text/html`,
12 ...requestCorsHeadersObject,
13 });
14 return res
15 .status(200)
16 .send(renderPlaygroundPage(playgroundRenderPageOptions));
17 }
18}

这里的逻辑主要是判断出本次请求是在请求 playground, 就渲染并返回

1return res.status(200).send(renderPlaygroundPage(playgroundRenderPageOptions));

renderPlaygroundPage函数来自于@apollographql/graphql-playground-html包, 应该是类似 express-graphql 中对 graphiql 的处理.

然后在最后, 返回被graphqlVercel处理过的函数:

1return graphqlVercel(async () => {
2 await promiseWillStart;
3 return this.createGraphQLServerOptions(req, res);
4})(req, res);

然后createHandler就结束了, 先看一下最终调用方式:

1const server = new ApolloServer({
2 typeDefs,
3 resolvers,
4 playground: true,
5 introspection: true,
6});
7
8export default server.createHandler();

也就是说, 调用 createHandler 方法返回了:

1async (req, res) => {
2 // ...
3 return graphqlVercel(async () => {
4 await promiseWillStart;
5 return this.createGraphQLServerOptions(req, res);
6 })(req, res);
7};

先去看看 graphqlVercel 是个啥:

vercelApollo.ts

1export function graphqlVercel(
2 options: GraphQLOptions | NowGraphQLOptionsFunction
3): NowApiHandler {
4 if (!options) throw new Error(`Apollo Server requires options.`);
5
6 if (arguments.length > 1) {
7 throw new Error(
8 `Apollo Server expects exactly one argument, got ${arguments.length}`
9 );
10 }
11
12 const graphqlHandler = async (req: NowRequest, res: NowResponse) => {
13 if (req.method === `POST` && !req.body) {
14 return res.status(500).send(`POST body missing.`);
15 }
16
17 try {
18 const { graphqlResponse, responseInit } = await runHttpQuery([req, res], {
19 method: req.method as string,
20 options,
21 query: req?.body || req.query,
22 request: convertNodeHttpToRequest(req),
23 });
24 setHeaders(res, responseInit.headers ?? {});
25 return res.status(200).send(graphqlResponse);
26 } catch (error) {
27 const { headers, statusCode, message }: HttpQueryError = error;
28 setHeaders(res, headers ?? {});
29 return res.status(statusCode).send(message);
30 }
31 };
32
33 return graphqlHandler;
34}
  • 接收一个函数, 这个函数会先执行 Apollo-Server 的 willStart 钩子, 然后返回创建的选项(从签名来看也可以直接传入选项, 但是要自己处理 willStart 吧)
  • 创建graphqlHandler函数, graphqlVercel接受的(req, res)就是给这个函数使用的, 然后内部其实就是调用runHttpQuery方法, 执行完请求之后就res.status(200).send(graphqlResponse)进行响应.

setHeaders.ts

这里导出了setHeaders方法, 在预检处理 onHealthCheck 还有 playground 的响应中都进行了调用, 先盲猜一手这个是设置响应头的, 因为调用方式是这样的:

1setHeaders(res, {
2 "Content-Type": `text/html`,
3 ...requestCorsHeadersObject,
4});

这个文件其实也很简单...

1import { NowResponse } from "@vercel/node";
2
3export const setHeaders = (
4 res: NowResponse,
5 headers: Record<string, any>
6): void => {
7 for (const [name, value] of Object.entries(headers)) {
8 res.setHeader(name, value);
9 }
10};

2333 这个猜不对感觉可以转行了