Skip to content

切换不同上下文

Nest 支持创建 HTTP 服务、WebSocket 服务和基于 TCP 通信的微服务。这些不同的服务都可以实现 Guard、Interceptor、Exception Filter 功能。

不同类型的服务能拿到的参数也是不同的。如 HTTP 服务有 Response 和 Request 服务,而 WebSocket 服务则没有。那怎么让功能跨多种上下文复用呢?

Nest 的解决方法是 ArgumentHost 和 ExecutionContext 类。

ArgumentHost

新建 Filter,catch 所有未捕获异常。

ts
export class AaaFilter implements ExceptionFilter {
  catch(exception, host: ArgumentsHost) {
    }
  }
}

创建一个自定义异常类。

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

在 filter 的 @Catch 装饰器里声明捕获异常。

ts
@Catch(AaaException)
export class AaaFilter implements ExceptionFilter {
  catch(exception, host: ArgumentsHost) {
    console.log(exception, host)
  }
}

在 Controller 中启用。

ts
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @UseFilters(AaaFilter)
  getHello(): string {
    throw new AaaException('aaa', 'bbb')
    // return this.appService.getHello();
  }
}

访问该路径,在控制台能看到 filter 被调用了。

exception 是异常调用对象,host 则是提供用于检索传递给处理程序的参数方法。

getArgs() 用于取出当前上下文的 request、response 和 next 参数;getArgsByIndex() 按数组下标取出参数;switchToRpc() 将上下文切换到 RPC;switchToHttp() 将上下文切换至 HTTP;switchToWs() 将上下文切换到 WebSocket;getType() 返回当前上下文类型。

一般的用法如下:

ts
@Catch(AaaException)
export class AaaFilter implements ExceptionFilter {
  catch(exception, host: ArgumentsHost) {
    if (host.getType() === 'http') {
      const ctx = host.switchToHttp()

      const res = ctx.getResponse<Response>()
      const req = ctx.getRequest<Request>()

      res.status(500).json({
        aaa: exception.aaa,
        bbb: exception.bbb,
      })
    }
  }
}

ExecutionContext

如果是 Guard 或 Interceptor,Nest 提供的是 ExecutionContext。

新建 Guard,可以看到传入的是 ExecutionContext。

ts
@Injectable()
export class AaaGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {}
}

ExecutionContext 是 ArgumentHost 的子类,在其基础上扩展了 getClassgetHandler 方法。Guard 和 Interceptor 的逻辑可能要根据目标的 class、handler 有没有某些装饰而进行处理。

比如权限验证的场景中,我们会定义一些角色。

ts
export enum Role {
  User = 'user',
  Admin = 'admin',
}

定义一个装饰器,作用是在目标上添加 roles 的 Metadata。

ts
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles)

在 Handler 上添加这个装饰器,参数为 admin,即添加了 roles 为 admin 的 Metadata。

ts
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @UseGuards(AaaGuard)
  @Roles(Role.Admin)
  getHello(): string {
    return this.appService.getHello()
  }
}

在 Guard 里就可以根据这个 Metadata 决定是否放行。

ts
@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))
  }
}

Interceptor 和 Guard 同理,不再赘述。

Released under the MIT License.