Nest.js 执行上下文
Nest 提供了几个实用程序类,有助于轻松编写跨多个应用程序上下文(例如,基于 HTTP 服务器的 Nest、微服务和 WebSockets 应用程序上下文)运行的应用程序。 这些实用程序提供有关当前执行上下文的信息,这些信息可用于构建可以跨广泛的控制器、方法和执行上下文工作的通用防护、过滤器和拦截器。
我们在本章中介绍了两个这样的类:ArgumentsHost
和 ExecutionContext
。
ArgumentsHost 类
ArgumentsHost
类提供用于检索传递给处理程序的参数的方法。 它允许选择适当的上下文(例如,HTTP、RPC(微服务)或 WebSockets)来检索参数。 该框架在我们可能想要访问它的地方提供了一个 ArgumentsHost 实例,通常作为主机参数引用。 例如,使用 ArgumentsHostinstance 调用异常过滤器的 catch() 方法。
ArgumentsHost 只是作为处理程序参数的抽象。 比如对于HTTP服务端应用(使用@nestjs/platform-express时),host对象封装了Express的[request, response, next]
数组,其中request为请求对象,response为响应对象,next为 控制应用程序的请求-响应周期的函数。 另一方面,对于 GraphQL 应用程序,宿主对象包含 [root, args, context, info]
数组。
当前应用程序上下文
在构建旨在跨多个应用程序上下文运行的通用防护、过滤器和拦截器时,我们需要一种方法来确定我们的方法当前正在其中运行的应用程序类型。使用 ArgumentsHost 的 getType() 方法执行此操作:
if (host.getType() === 'http') {
// 做一些只在常规 HTTP 请求 (REST) 上下文中重要的事情
} else if (host.getType() === 'rpc') {
// 做一些只在微服务请求的上下文中重要的事情
} else if (host.getType<GqlContextType>() === 'graphql') {
// 做一些只在 GraphQL 请求的上下文中重要的事情
}
提示
: GqlContextType 是从 @nestjs/graphql 包中导入的。
有了可用的应用程序类型,我们可以编写更多的通用组件,如下所示。
主机处理程序参数
要检索传递给处理程序的参数数组,一种方法是使用宿主对象的 getArgs() 方法。
const [req, res, next] = host.getArgs();
我们可以使用 getArgByIndex() 方法按索引提取特定参数:
const request = host.getArgByIndex(0);
const response = host.getArgByIndex(1);
在这些示例中,我们按索引检索请求和响应对象,通常不建议这样做,因为它将应用程序耦合到特定的执行上下文。 相反我们可以通过使用宿主对象的实用程序方法之一来为应用程序切换到适当的应用程序上下文,从而使代码更加健壮和可重用。 上下文切换实用程序方法如下所示。
/**
* Switch context to RPC.
*/
switchToRpc(): RpcArgumentsHost;
/**
* Switch context to HTTP.
*/
switchToHttp(): HttpArgumentsHost;
/**
* Switch context to WebSockets.
*/
switchToWs(): WsArgumentsHost;
让我们使用 switchToHttp()
方法重写前面的示例。 host.switchToHttp() 帮助器调用返回一个适用于 HTTP 应用程序上下文的 HttpArgumentsHost 对象。 HttpArgumentsHost 对象有两个有用的方法,我们可以使用它来提取所需的对象。 在这种情况下,我们还使用 Express 类型断言来返回本机 Express 类型对象:
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
类似地,WsArgumentsHost
和 RpcArgumentsHost
具有在微服务和 WebSockets 上下文中返回适当对象的方法。 以下是 WsArgumentsHost 的方法:
export interface WsArgumentsHost {
/**
* 返回 data 对象.
*/
getData<T>(): T;
/**
* 返回 client 对象.
*/
getClient<T>(): T;
}
以下是 RpcArgumentsHost
的方法:
export interface RpcArgumentsHost {
/**
* 返回 data 对象.
*/
getData<T>(): T;
/**
* 返回 context 对象.
*/
getContext<T>(): T;
}
ExecutionContext 类
ExecutionContext
继承 ArgumentsHost,提供有关当前执行过程的其他详细信息。 与 ArgumentsHost 一样,Nest 在我们可能需要它的地方提供了一个 ExecutionContext 实例,例如在守卫的 canActivate() 方法和拦截器的intercept() 方法中。 它提供了以下方法:
export interface ExecutionContext extends ArgumentsHost {
/**
* Returns the type of the controller class which the current handler belongs to.
*/
getClass<T>(): Type<T>;
/**
* Returns a reference to the handler (method) that will be invoked next in the
* request pipeline.
*/
getHandler(): Function;
}
getHandler() 方法返回对即将被调用的处理程序的引用。 getClass() 方法返回此特定处理程序所属的 Controller 类的类型。 例如,在 HTTP 上下文中,如果当前处理的请求是 POST 请求,绑定到 CatsController 上的 create() 方法,getHandler() 返回对 create() 方法的引用,而 getClass() 返回 CatsControllertype(不是 实例)。
const methodKey = ctx.getHandler().name; // "create"
const className = ctx.getClass().name; // "CatsController"
访问对当前类和处理程序方法的引用的能力提供了极大的灵活性。 最重要的是,它让我们有机会通过 @SetMetadata()
装饰器从 guard 或拦截器中访问元数据集。 我们将在下面介绍这个用例。
反射和元数据
Nest 提供了通过 @SetMetadata()
装饰器将自定义元数据附加到路由处理程序的能力。 然后我们可以从我们的类中访问这个元数据来做出某些决定。
cats.controller.ts
@Post() @SetMetadata('roles', ['admin']) async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); }
提示
: @SetMetadata() 装饰器是从 @nestjs/common 包中导入的。
通过上面的构造,我们将角色元数据(roles 是元数据键,['admin'] 是关联值)附加到 create() 方法。 虽然这可行,但在我们的路由中直接使用 @SetMetadata() 并不是一个好习惯。 相反,创建自己的装饰器,如下所示:
roles.decorator.ts
import { SetMetadata } from '@nestjs/common'; export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
这种方法更干净、更易读,并且是强类型的。 现在我们有了一个自定义的 @Roles()
装饰器,我们可以用它来装饰 create() 方法。
cats.controller.ts
@Post() @Roles('admin') async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); }
要访问路由的角色(自定义元数据),我们将使用 Reflector
辅助类,它由框架开箱即用地提供,并包含在 @nestjs/core
包中,是可以公开访问的。 反射器可以使用正常方式注入到一个类中:
roles.guard.ts
@Injectable() export class RolesGuard { constructor(private reflector: Reflector) {} }
提示
: Reflector 类是从 @nestjs/core 包中导入的。
现在,要读取处理程序元数据,请使用 get() 方法。
const roles = this.reflector.get<string[]>('roles', context.getHandler());
Reflector#get
方法允许我们通过传入两个参数轻松访问元数据:元数据键和用于从中检索元数据的上下文(装饰器目标)。 在这个例子中,指定的键是'roles'(参考上面的 roles.decorator.ts 文件和在那里进行的SetMetadata() 调用)。 上下文由对 context.getHandler()
的调用提供,这导致为当前处理的路由处理程序提取元数据。 请记住,getHandler() 为我们提供了对路由处理函数的引用。
或者,我们可以通过在控制器级别应用元数据来组织我们的控制器,应用于控制器类中的所有路由。
cats.controller.ts
@Roles('admin') @Controller('cats') export class CatsController {}
在这种情况下,为了提取控制器元数据,我们将 context.getClass()
作为第二个参数传递(以提供控制器类作为元数据提取的上下文)而不是 context.getHandler():
roles.guard.ts
const roles = this.reflector.get<string[]>('roles', context.getClass());
鉴于能够在多个级别提供元数据,我们可能需要从多个上下文中提取和合并元数据。 Reflector 类提供了两个实用方法来帮助解决这个问题。 这些方法一次提取控制器和方法元数据,并以不同的方式组合它们。
考虑以下场景,我们在两个级别都提供了“roles”元数据。
cats.controller.ts
@Roles('user') @Controller('cats') export class CatsController { @Post() @Roles('admin') async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } }
如果我们的意图是将“用户”指定为默认角色,并为某些方法选择性地覆盖它,则可能会使用 getAllAndOverride() 方法。
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
具有此代码的 Guard ,在 create() 方法的上下文中运行,具有上述元数据,将导致角色包含 ['admin']。
要获取两者的元数据并将其合并(此方法合并数组和对象),请使用 getAllAndMerge() 方法:
const roles = this.reflector.getAllAndMerge<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
这将导致角色包含 ['user', 'admin']
。
对于这两种合并方法,我们将元数据键作为第一个参数传递,并将元数据目标上下文数组(即,对 getHandler() 和/或 getClass()) 方法的调用)作为第二个参数传递。