nestjs学习12:ExecutionContext 切换不同上下文

30 阅读3分钟

Nest 支持创建多种类型的服务:包括 HTTP 服务、WebSocket 服务,还有基于 TCP 通信的微服务。

这三种服务都会支持 Guard、Interceptor、Exception Filter 功能。

那么问题来了:

不同类型的服务它能拿到的参数是不同的,比如 http 服务可以拿到 request、response 对象,而 websocket 服务就没有。

也就是说你不能在 Guard、Interceptor、Exception Filter 里直接操作 request、response,不然就没法复用了,因为 websocket 没有这些。

那如何让同一个 Guard、Interceptor、Exception Filter 在不同类型的服务里复用呢?

Nest 的解决方法是 ArgumentHost 这个类。

创建一个 filter:

nest g filter aaa --flat --no-spec

image.png

通常 Nest 会 catch 所有未捕获异常。如果是 Exception Filter 声明的异常,那就会调用自定义的filter 来处理, 比如: AaaFilter

那 filter 怎么声明捕获什么异常的呢?

我们创建一个自定义的异常类:

export class AaaException {
    constructor(public aaa: string, public bbb: string) {
        
    }
}

在 @Catch 装饰器里声明这个 filter 来处理该异常:

image.png

然后需要启用它:

image.png

路由级别启用 AaaFilter,并且在 handler 里抛了一个 AaaException 类型的异常。

也可以全局启用:

image.png

访问 http://localhost:3000 就可以看到 filter 被调用了。

image.png

filter 的第一个参数就是异常对象,那第二个参数呢?

可以看到,它有这些方法:

image.png

在调试模式下 debug console 输入 host,可以看到它有这些属性方法:

image.png

host.getArgs 方法就是取出当前上下文的 reqeust、response、next 参数。

因为当前上下文是 http 服务,如果是 WebSocket 服务,这里拿到的就是别的东西了。

调用 switchToHttp 切换到 http 上下文,然后再调用 getRequest、getResponse 方法。

如果是 websocket、基于 tcp 的微服务等上下文,就分别调用 host.swtichToWs、host.switchToRpc 方法。

这样,就可以在 filter 里处理多个上下文的逻辑,跨上下文复用 filter了。

加个 if else 判断即可:

import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
import { AaaException } from './AaaException';

@Catch(AaaException)
export class AaaFilter implements ExceptionFilter {
  catch(exception: AaaException, host: ArgumentsHost) {
    if(host.getType() === 'http') {
      const ctx = host.switchToHttp();
      const response = ctx.getResponse<Response>();
      const request = ctx.getRequest<Request>();

      response
        .status(500)
        .json({
          aaa: exception.aaa,
          bbb: exception.bbb
        });
    } else if(host.getType() === 'ws') {

    } else if(host.getType() === 'rpc') {

    }
  }
}

刷新页面,就可以看到 filter 返回的响应:

image.png

所以说,ArgumentHost 是用于切换 http、websocket、rpc 等上下文类型的,可以根据上下文类型取到对应的 argument,让 Exception Filter 等在不同的上下文中复用

那 guard 和 interceptor 里呢?

创建个 guard 试一下:

image.png

有这些方法:

image.png

是不是很眼熟?

没错,ExecutionContext 是 ArgumentHost 的子类,扩展了 getClass、getHandler 方法。

image.png

多加这两个方法是干啥的呢?

这俩分别是要调用的 controller 的 class 以及要调用的方法。

image.png

为什么 ExecutionContext 里需要拿到目标 class 和 handler 呢?

因为 Guard、Interceptor 的逻辑可能要根据目标 class、handler 有没有某些装饰而决定怎么处理。

比如权限验证的时候,我们会先定义几个角色:

image.png

然后定义这样一个装饰器:

image.png

它的作用是往修饰的目标上添加 roles 的 metadata。

然后在 handler 上添加这个装饰器,参数为 admin,也就是给这个 handler 添加了一个 roles 为 admin 的metadata。

image.png

这样在 Guard 里就可以根据这个 metadata 决定是否放行了:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { Role } from './role';

@Injectable()
export class AaaGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {

    const requiredRoles = this.reflector.get<Role[]>('roles', context.getHandler());

    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user && user.roles?.includes(role));
  }
}

我们在 Guard 里通过 ExecutionContext 的 getHandler 方法拿到了目标 handler 方法。

然后通过 reflector 的 api 拿到它的 metadata。

判断如果没有 roles 的 metadata 就是不需要权限,那就直接放行。

如果有,就是需要权限,从 user 的 roles中判断下有没有当前 roles,有的话就放行。

同样,在 interceptor 里也有这个:

image.png