直接讲解, 过渡 & 人话 & 介绍 & 总结 明天补上 自己领悟

总共就四个文件:

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

Index.ts

export { ApolloServer } from "./ApolloServer"
export type { CreateHandlerOptions } from "./ApolloServer"

入口文件, 不做讲解

ApolloServer.ts

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

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

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

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

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

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

首先生成一个空的Header类:

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

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

 if (cors) {
      if (cors.methods) {
        // 设置 access-control-allow-methods字段
      }
      if (cors.allowedHeaders) {
       // 设置 access-control-allow-headers 字段
      }
      if (cors.exposedHeaders) {
       // 设置 access-control-expose-headers 字段
      }
      if (cors.credentials) {
        corsHeaders.set(`access-control-allow-credentials`, `true`);
      }
      if (typeof cors.maxAge === `number`) {
        corsHeaders.set(`access-control-max-age`, cors.maxAge.toString());
      }
    }

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

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

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

内部:

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

const requestCorsHeaders = new Headers(corsHeaders);

cors.origin做处理:

if (cors && cors.origin) {
        const requestOrigin = req.headers.origin;
        if (typeof cors.origin === `string`) {
          requestCorsHeaders.set(`access-control-allow-origin`, cors.origin);
        } else if (
          requestOrigin &&
          (typeof cors.origin === `boolean` ||
            (Array.isArray(cors.origin) && requestOrigin && cors.origin.includes(requestOrigin as string)))
        ) {
          requestCorsHeaders.set(`access-control-allow-origin`, requestOrigin as string);
        }
        const requestAccessControlRequestHeaders = req.headers[`access-control-request-headers`];
        if (!cors.allowedHeaders && requestAccessControlRequestHeaders) {
          requestCorsHeaders.set(`access-control-allow-headers`, requestAccessControlRequestHeaders as string);
        }
      }

然后将其拼装为对象:

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

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

然后快速通过OPTIONS请求:

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

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

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

 if (this.playgroundOptions && req.method === `GET`) {
        const acceptHeader = req.headers.Accept || req.headers.accept;
        if (acceptHeader && acceptHeader.includes(`text/html`)) {
          const path = req.url || `/`;
          const playgroundRenderPageOptions: PlaygroundRenderPageOptions = {
            endpoint: path,
            ...this.playgroundOptions
          };
          setHeaders(res, {
            "Content-Type": `text/html`,
            ...requestCorsHeadersObject
          });
          return res.status(200).send(renderPlaygroundPage(playgroundRenderPageOptions));
        }
      }

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

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

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

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

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

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

const server = new ApolloServer({
  typeDefs,
  resolvers,
  playground: true,
  introspection: true
});
export default server.createHandler();

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

async (req, res) => {
  // ...
   return graphqlVercel(async () => {
        await promiseWillStart;
        return this.createGraphQLServerOptions(req, res);
      })(req, res);
}

先去看看graphqlVercel是个啥:

vercelApollo.ts

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

setHeaders.ts

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

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

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

import { NowResponse } from "@vercel/node"
export const setHeaders = (
  res: NowResponse,
  headers: Record<string, any>
): void => {
  for (const [name, value] of Object.entries(headers)) {
    res.setHeader(name, value)
  }
}

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