Skip to content

执行上下文 (ExecutionContext)

当然!我们来探索一个在之前的学习中反复出现,但我们从未给过它“主角光环”的核心概念:执行上下文 (ExecutionContext)。它是 NestJS 中许多强大功能(如守卫、拦截器、自定义装饰器)的“信息中枢”和“瑞士军刀”。

NestJS 的“上帝视角”:深入解析执行上下文 (ExecutionContext)

想象一下你是一个经验丰富的活动保安(一个 守卫 Guard)。你的职责是决定是否让某位访客(一个 请求 Request)进入某个特定的房间(一个 路由处理函数 Handler)。

为了做出正确的判断,你光站在那里是不够的。你需要一个高科技的平板电脑,上面显示着关于当前情况的所有信息:

  • 访客信息:这位访客是谁?(HTTP 请求的 request 对象)
  • 目标房间:他想进入哪个房间?(将要被执行的 Controller 方法)
  • 所在楼层:这个房间在哪一层?(这个方法所属的 Controller 类)
  • 活动类型:这是个正式晚宴,还是一个休闲派对?(是 HTTP 请求,还是 WebSocket 消息?)

在 NestJS 中,ExecutionContext 就是你手中的那台无所不知的“高科技平板电脑”。它是一个在守卫、拦截器、自定义装饰器等组件中被传递的参数,封装了当前执行操作的全部上下文信息。

1. 为什么它如此重要?—— 编写通用的“瑞士军刀”

ExecutionContext 最伟大的设计思想在于,它让 NestJS 的核心功能(守卫、拦截器等)变得与协议无关

这意味着,你可以编写一个 RolesGuard,它既能用于保护一个 HTTP 的 REST API 接口,也能用于保护一个 WebSocket 的实时消息处理程序。因为无论请求来自哪里,ExecutionContext 都会用一套统一的 API 把你需要的信息提供给你。

它就像一把瑞士军刀,虽然外表只有一个,但里面藏着针对不同场景的各种工具(螺丝刀、小刀、开瓶器)。

2. “瑞士军刀”里有什么工具?—— 核心方法解析

ExecutionContext 继承自 ArgumentsHost,并提供了几个非常有用的方法来获取当前执行的详细信息。

getHandler(): Function

  • 它是什么:获取即将被调用的路由处理函数(Controller 的方法)的引用。
  • 它有什么用:可以用来结合 Reflector 获取附加在该方法上的元数据(比如 @Roles('admin'))。

getClass<T>(): Type<T>

  • 它是什么:获取该路由处理函数所属的 Controller 类的引用。
  • 它有什么用:可以用来获取附加在整个 Controller 类上的元数据。

getType<TContext extends string = ContextType>(): TContext

  • 它是什么:获取当前执行上下文的类型。返回值是 'http', 'rpc', 或 'ws'
  • 它有什么用:让你可以在同一个组件中,根据不同的请求类型执行不同的逻辑。

代码示例:一个“信息侦探”守卫

让我们创建一个守卫,它的唯一目的就是利用这些方法来“侦察”它所保护的路由。

src/info.guard.ts

typescript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class InfoGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const contextType = context.getType();
    console.log(`侦测到上下文类型: ${contextType}`);

    // 只有在 HTTP 上下文中我们才能安全地获取 Controller 和 Handler
    if (contextType === 'http') {
      const controllerClass = context.getClass();
      const handlerFunction = context.getHandler();

      console.log(`目标 Controller 类名: ${controllerClass.name}`);
      console.log(`目标处理函数名: ${handlerFunction.name}`);
    }

    return true; // 我们只是侦察,所以总是放行
  }
}

在控制器中使用它:

typescript
// cats.controller.ts
@UseGuards(InfoGuard)
@Controller('cats')
export class CatsController {
  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}

当你访问 /cats 时,控制台会输出:

侦测到上下文类型: http
目标 Controller 类名: CatsController
目标处理函数名: findAll

3. 打开正确的“工具”:switchToHttp() 等切换方法

getHandler()getClass() 很有用,但它们没有给我们最重要的东西:原始的请求(Request)和响应(Response)对象

这就是切换上下文方法大显身手的地方。ExecutionContext 提供了这些方法,让你从通用的“平板电脑”界面,切换到针对特定协议的“详细视图”。

  • switchToHttp(): 返回一个 HttpArgumentsHost 对象,可以从中获取 getRequest()getResponse()
  • switchToWs(): 返回一个 WsArgumentsHost 对象,可以获取 getData()getClient()
  • switchToRpc(): 返回一个 RpcArgumentsHost 对象,可以获取 getData()

最常用、也最重要的就是 switchToHttp()

实战演练:一个真正的 AuthGuard

这个守卫需要检查 HTTP 请求的 headers 中是否包含一个有效的 Authorization 令牌。

typescript
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // 1. 从通用上下文切换到 HTTP 上下文
    const httpContext = context.switchToHttp();

    // 2. 从 HTTP 上下文中获取 request 对象
    const request = httpContext.getRequest();

    // 3. 现在你可以像在 Express 中一样操作 request 对象了!
    const authToken = request.headers['authorization'];

    if (authToken && authToken.startsWith('Bearer ')) {
      console.log('令牌存在,验证通过!');
      return true; // 实际项目中这里会验证令牌的有效性
    }

    console.log('未找到令牌,拒绝访问!');
    return false;
  }
}

这个例子完美地展示了 ExecutionContext 的核心工作流程:

  1. 接收通用的 context
  2. 调用 switchToHttp() 切换到 HTTP 专属的“工具集”。
  3. 调用 getRequest() 拿到我们最熟悉的 request 对象。
  4. 执行具体的业务逻辑。

4. ExecutionContext 与自定义装饰器的珠联璧合

我们之前学习自定义装饰器时,其实已经和 ExecutionContext 打过照面了!创建自定义参数装饰器的工厂函数,接收的第二个参数就是 ExecutionContext

让我们重新审视 @User() 装饰器的实现,这次你会看得更明白。

@User() 装饰器:

typescript
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  // 这个工厂函数的第二个参数就是 ExecutionContext!
  (data: unknown, ctx: ExecutionContext) => {
    // 我们在这里做的,和在 Guard 里做的事情一模一样
    const request = ctx.switchToHttp().getRequest();
    // 装饰器的返回值,就是将被注入到 Controller 方法参数中的值
    return request.user;
  },
);

现在你应该恍然大悟了:自定义装饰器之所以能从请求中提取数据,正是因为 NestJS 把包含了所有请求信息的“平板电脑”——ExecutionContext——交给了它。

总结

ExecutionContext 是 NestJS 依赖注入和请求处理流程中一个至关重要的“枢纽”。

它是什么?一个包含了当前执行操作所有信息的参数对象。
谁在使用它?守卫 (Guards), 拦截器 (Interceptors), 自定义装饰器 (Custom Decorators), 以及 ModuleRef
它的超能力?协议无关。用一套统一的 API (getType, getClass...) 应对不同的通信协议。
最核心的操作?通过 switchToHttp() 切换到 HTTP 上下文,然后调用 getRequest() 来获取原始的请求对象。

理解了 ExecutionContext,你才算真正理解了 NestJS 的守卫和拦截器等功能是如何与底层的 HTTP 服务器(如 Express 或 Fastify)解耦,并协同工作的。它不是你每天都会直接操作的对象,但它无处不在,是你编写高级、可复用 NestJS 组件的基石。