开发环境
- @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)
,args
,context
,info
(顺序固定)。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 替代是为了方便识别,名字不重要。
解析器参数
ARGUMENT | DESCRIPTION |
---|---|
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] |
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 接口性能的参考,关于性能统计部分会再写其他文章讲解。