@nestjs/graphql 及Apollo Server源码解析——上下文

301 阅读3分钟

开发环境

  • @nestjs/core 8.x
  • @nestjs/graphql 9.x,v10 有重大更新,apollo 模块分离出来了,需要单独安装 @nestjs/apollo 包
  • apollo-server-core 3.x

深入理解GraphQL 执行上下文

在 NestJS 中,GraphQL 的执行上下文与普通 REST接口的不同,它的类型属性 contextType 值为 'graphql',可以通过 context.getType() 方法获得。将普通的执行上下文转化为 GraphQL 执行上下文,需通过特定方法:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql'; // 1:导入类

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const gqlContext = GqlExecutionContext.create(context); // 2:转化为graphql执行上下文
    return true;
  }
}

Apollo GraphQL 解析器有一个单独的参数集(args[]),包含四个参数 root(parent)argscontextinfo(顺序固定)。gqlContext 的打印对象如下:

// query ‘myDownloadTemplates’ in graphql 
ExecutionContextHost {
  args: [
    undefined, // <== root(parent)
    { limit: 10, offset: 0 }, // <== args
    { // <== context
      req: [IncomingMessage],
      pubsub: [PubSub],
      NEST_LOADER_CONTEXT_KEY: [NestDataLoaderContext]
    },
    { // <== info
      fieldName: 'myDownloadTemplates',
      fieldNodes: [Array],
      returnType: CreativeEventResponse!,
      parentType: Query,
      path: [Object],
      schema: [GraphQLSchema],
      fragments: [Object: null prototype] {},
      rootValue: undefined,
      operation: [Object],
      variableValues: [Object],
      cacheControl: [Object]
    }
  ],
  constructorRef: [class CreativeEventResolver],
  handler: [AsyncFunction: myDownloadTemplates],
  contextType: 'graphql'
}

root 参数在 graphql 中的原始定义是上一级对象,Apollo 用 parent 替代是为了方便识别,名字不重要。

解析器参数

ARGUMENTDESCRIPTION
parent该字段的父级解析器的返回值(即解析器链中的前一个解析器)

对于没有父级的顶级字段的解析器(就像 Query 字段),这个值是从传递给 Apollo Server 构造函数rootValue 函数中获取的。
args包含了为该字段提供的所有 GraphQL 参数的对象。

例如,当执行 query{ user(id: "4") }args 对象传递给 user 解析器的是 { "id": "4" }
context在为特定操作而执行的解析器之间共享的一个对象。用以共享每个操作的状态,包括验证信息,dataloader 实例,以及要跨解析器跟踪的任何其他内容。

更多信息请看下文 context 参数详解。
info包含查询操作的执行状态信息,包括字段名称、从根节点到该字段的路径等。

它的核心字段被列在 GraphQL.js 源码中,并通过其他模块扩展了附加功能,例如 appllo-cache-control

这四个参数的在 gqlContext上的调用方法分别为:getRoot()getArgs()getContext()getInfo()。另外 @nestjs/graphql 中对这些方法进行了包装,对应的专用装饰器如下表所示:

@nestjs/graphql 装饰器apollo 参数
@Root()@Parent()root/parent
@Context(param?:string)context/context[param]
@Info(param?:string)info/info[param]
@Args(param?:string)args/args[param]

wolai-minder-n3Hg6EAFaFz8vmYaFGfJjP-kBn79sxnH2D92NP8gZfnXx.png

Context 参数详解

context 参数对于传递给任何解析器可能需要的东西很有用,像认证范围、数据库连接、以及自定义请求函数等。如果你跨解析器使用dataloaders批量处理请求,你可以同样将它们附加到context上。

解析器永远不应破坏性地修改 context 参数。这样才能确保所有解析器的一致性并防止意外错误。

要提供一个 context给你的解析器,需要给ApolloServer构造器添加一个context初始化函数。此函数被所有请求调用,所以你可以在请求详情的基础上设置上下文(就像HTTP头信息)。

在NestJS createGqlOptions中初始化 context

context: ({ req, connection }) => {
      // connection.context will be equal to what was returned by the "onConnect" callback
      // 9.0版本中已经没有 subscribe connection 这个对象了
      if (connection) {
          return { req: connection.context, pubSub };
      }
      return { req, pubSub, usersLoader }; // <== 此处将 req、pubSub、userLoader 附加到 context
  }

应用延伸

在 NestJS 中,理解了 graphql 的执行上下文,就可以像 REST应用一样使用守卫、拦截器、过滤器、管道、自定义装饰器等获取执行上下文,并进行 AOP 截取切面编程。这些应用参考官方给出的文档案例,查看此处

案例:兼容 GraphQL 请求到日志拦截器设计

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
import { map, Observable, tap } from 'rxjs';
import clog from 'custom-logger';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {
        // REST API
        if (context.getType() === 'http') {
            const now = Date.now();
            const request = context.switchToHttp().getRequest();
            const method = request.method;
            const url = request.url;
            const body = request.body;
            const parmas = request.parmas;
            const query = request.query;
            const user = request.user || undefined;

            // 跳过健康检查
            if (url === '/vi/health') {
                return next.handle();
            }

            const trace = {
                body,
                parmas,
                query,
                user,
            };

            return next.handle().pipe(
                tap(() => {
                    clog.custom({
                        type: 'info',
                        title: `REST API LOG:${context.getClass().name}`,
                        message: `${method} ${url} ${Date.now() - now}ms`,
                        addInfo: trace,
                    });
                }),
            );
        }

        // GraphQL
        if (context.getType<GqlContextType>() === 'graphql') {
            const now = Date.now();
            const gqlContext = GqlExecutionContext.create(context);
            const info = gqlContext.getInfo();
            const ctx = gqlContext.getContext();
            const args = gqlContext.getArgs();
            const user = ctx.req.user;
            const parentType = info.parentType.name;
            const fieldName = info.fieldName;
            const body = info.fieldNodes[0]?.loc?.source?.body;
            const message = `GraphQL - ${parentType} - ${fieldName}`;

            const trace = {
                userId: user ? user.id : undefined,
                body,
                args,
            };

            return next.handle().pipe(
                tap(() => {
                    clog.custom({
                        type: 'info',
                        title: `GraphQL LOG:${context.getClass().name}`,
                        message: `${message} ${Date.now() - now}ms`,
                        addInfo: trace,
                    });
                }),
                map((data) => {
                    // 可对返回值进行操作
                    return data;
                }),
            );
        }

        // 其他类型请求
        return next.handle();
    }
}

这里的 GraphQL 响应耗时只是针对单个解析器节点,拦截器无法获取整个 Apollo Server 解析器链的事件,因此这个耗时不能作为统计 GraphQL 接口性能的参考,关于性能统计部分会再写其他文章讲解。