直接讲解, 过渡 & 人话 & 介绍 & 总结
明天补上自己领悟
总共就四个文件:
- index.ts
- ApolloServer.ts
- setHeaders.ts
- vercelApollo.ts
Index.ts
export { ApolloServer } from "./ApolloServer"
export type { CreateHandlerOptions } from "./ApolloServer"
入口文件, 不做讲解
ApolloServer.ts
先去掉了文件上传相关的代码实现
继承了apollo-server
中导出的ApolloServerBase
类, 然后实现了createGraphQLServerOptions
与createHandler
方法, 第一个:
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 这个猜不对感觉可以转行了