Nestjs 核心特性、基本概念、生命周期、获取请求和响应、不同组件的实现和执行顺序

Posted by CodingWithAlice on December 26, 2024

Nestjs 核心特性、基本概念、生命周期、获取请求和响应、不同组件的实现和执行顺序

总结:

  • 核心特性:模块化架构 + 依赖注入DI + 多种协议 HTTP/websocket
  • 基本概念:控制器 controller + 根模块/子模块 module + 服务 provide + 应用程序入口 main.ts
    • 在模块中配置,将服务注入控制器
    • 令牌:类是代码的具体实现,令牌是依赖注入的 用于标识依赖项的唯一标识,通过令牌请求相应实例
    • 装饰器:一个表达式,返回一个函数,参数为目标、名称、属性描述符 createParamDecorator
  • Nest 生命周期:
    • ①应用初始化:NestJS 框架启动,解析模块、控制器、provider 等配置信息[通过装饰器@Module @Controller @Injectable 定义的一些元数据,定义了各个组件],构建依赖[收集、整理组件间的依赖关系]注入容器
      • 容器:框架内部用于管理组件和依赖关系的一个抽象概念,应用启动时创建
    • ②依赖解析和实例化:根据配置,解析各组件的依赖关系[深入分析],根据关系依次实例化 provider、实例化控制器
    • ③应用启动:所有组件实例化后,应用监听指定端口,正式提供服务
    • ④请求处理:运行过程中,接收请求,按照中间件-守卫-拦截器-管道-拦截器顺序处理请求,返回响应
    • ⑤应用关闭:执行关闭操作,销毁各组件实例
  • 不同组件的生命周期:
    • Provider:应用启动时,NestJS 依赖注入系统解析依赖关系,实例化每个 provider[一般为单例模式];在应用关闭时,实例被销毁
    • 模块:NestJS 提供了生命周期钩子 onModuleInit(所有provider实例化后调用) - beforeApplicationShutdown(应用关闭前)-onModuleDestroy(模块销毁时)
    • 控制器:和 provider 相似,在应用启动时实例化,应用关闭时销毁
  • Nest 有两种响应方式
    • 标准情况:若是对象/数组,则自动序列化为 JSON;若为基础类型,直接发送,不序列化
    • 类库特有类型:例如 Express 的 response.status(200).send()
  • Nest 获取请求参数的三种方式
    • 1、@Req():Express 的底层,包含请求行、请求头、请求体
    • 2、@Body、@Guery:开箱即用
    • 3、@Param:动态路由参数
  • 不同组件的执行顺序:中间件(全局请求的预处理) -> 守卫(权限验证) -> 拦截器(请求阶段-灵活拦截和阻拦) -> 管道(输入参数的转换和验证) -> 控制器处理程序 -> 拦截器(响应阶段)
    • 全局配置 app.use* 方式 - 在 main.ts 中引入配置 app.use*(类/实例)
    • 全局配置 useClass 方式 - 在 app.module.ts 配置 @Module 的配置参数 providers 声明令牌和类 [{ provide: API_*, useClass: 类 }]
    • 控制器范围配置 @Use* 方式 - 在 *.controler.ts 的路由方法 @Use*(类/实例)
组件(按序) 功能 时机 实现 配置
中间件 全局请求的预处理,可以访问原生请求和响应,日志记录、请求验证、请求解析、跨域处理 在请求进入NestJS路由系统之前 ①函数中间件logger(req,res,next)
②类...implements NestMiddleware{ use(req,res,next){}}
①全局配置 app.use 方式
②模块 - 在 *.module.ts 导出类 ...implements NestModule{configure(consumer){consumer.apply(Logger).forRoutes('cats');}}
守卫 权限验证,决定请求是否可以继续处理   ...implements CanActivate{ canActivate(context){}}返回布尔值/可以解析为布尔的Promise ①全局配置 app.use* 方式 - app.useGlobalGuards
全局配置 useClass 方式
②控制器范围配置 @Use* 方式 - @UseGards
拦截器(请求阶段) 添加额外的请求头、修改请求参数等 请求到达控制器处理程序之前 ...implements NestInterceptor{ intercept(ctx, next){} } ①全局配置 app.use* 方式 - app.useGlobalInterceptors
全局配置 useClass 方式
②控制器范围配置 @Use* 方式 - @UseInterceptors
管道 控制器处理程序的输入参数 进行转换和验证,验证路由参数、查询字符串参数、请求体格式等   ①内置管道9个:6个 Parse* 管道
②自定义管道 - 返回的值完全覆盖原值 ...implements PipeTransform<T, R>{transform(value: T, metadata):R{}}
①全局配置 app.use* 方式 - app.useGlobalPipes
全局配置 useClass 方式
②控制器范围配置 @Use* 方式 - @UsePipes
③路由处理函数 - 在参数处理中使用管道 @Query/Param('property', 多个类/实例) @Body(类/实例)
控制器处理程序 业务代码      
拦截器(响应阶段) 对响应数据进行拦截和处理,例如统一响应格式、日志记录      

和 Nextjs 区别:

  • Nextjs - 前后端一体的框架 = 构建服务器渲染 SSR + 静态网站生成SSG
    • 核心特性是 = ①SSR:「服务端动态渲染」页面,将生成的 HTML 发送给客户端 ②SSG:构建阶段就能够生成 HTML - 预渲染 ③代码自动分割 - 按照路由分割 ④基于文件系统的路由
  • Nestjs - 后端框架,提供 API 服务 = Nodejs + TS
    • 核心特性 = ①模块化架构 ②依赖注入(DI) ③支持多种协议 HTTP websocket

Nextjs 更关注前端的性能优化、数据获取,Nestjs 更关注后端架构、数据库连接等

下文总结于 Nestjs中文文档,便于常看常新

一、结构

基本结构配置补充:

src
 ├── app.controller.spec.ts // 对于基本控制器的「单元测试」样例
 ├── app.controller.ts      // 带有单个路由的「基本控制器」@Controller
 ├── app.module.ts 			// 「根模块」@Module
 ├── app.service.ts			// 带有单个方法的基本服务(Providers) @Injectable
 └── main.ts				// 应用程序入口文件 - 使用 NestFactory 用来创建 Nest 应用实例

关系描述:

  • 控制器将更复杂的任务委托给 providers -> (依赖注入)「服务」注入「控制器」,被调用
  • 在「根模块」注册「服务」,以便执行注入控制器
  • 提供者「服务」可以注入到模块类(「子模块」)中,但由于循环依赖性,子模块不能注入到服务中
    • 在 Nest 中,默认情况下,模块是单例 -> 实际上,每个模块都是一个共享模块( exports 配置)
    • 提供者「服务」被封装在模块范围内
  • 中间件会早于管道执行
    • 中间件侧重于对请求的初步处理和全局操作,而管道则专注于对控制器输入参数的转换和验证
  • 守卫在每个中间件之后执行,但在任何拦截器或管道之前执行
  • 使用 @Injectable 的模块:provider 、中间件、管道、守卫、拦截器
  • Nest 是基于装饰器这种语言特性而创建的

NestJS 流程:

  • NestJS 运行时系统实例化 CatsController 时,先查找所有依赖项,自上而下,根据依赖关系图解析;找到 CatsService 依赖项时查找返回 CatsService 类,创建 CatsService 实例,并将其缓存并返回(若已缓存,则直接返回现有实例)

关键理解:

  • 令牌:类是代码的具体实现,令牌是 依赖注入系统 中 用于标识依赖项的唯一标识。当某个组件,例如控制器、服务需要另一个组件(例如服务)时,会 通过令牌来请求相应的实例

      providers: [CatsService] // 简写时,NestJS默认使用类名 CatsService 作为令牌
      providers: [{ provide: CatsService, useClass: CatsService }] // 具体写明时,provide 明确指定令牌 - 标识, useClass 指定了要实例化的类
      providers: [{ provide: API_KEY, useValue: 'your-api-key' }] // 注入常量或值也需要令牌
      -- // 在服务中注入常量
      constructor(@Inject(API_KEY) private readonly apiKey: string) {}
    

1、 跨域配置 main.ts

解决办法:在 main.ts 配置 CORS

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';

async function bootstrap() {
    const app = await NestFactory.create(AppModule); // 创建应用实例
    const corsOptions: CorsOptions = {
        // 配置 CORS 选项,允许来自 4001 端口的请求
        origin: ['http://localhost:4001', 'http://codingwithalice.top:4001'], 
        methods: 'GET,PUT,PATCH,POST',
    };
    app.enableCors(corsOptions);
    // 还有下文会用到的一些配置:app.use(c);  - 函数式中间件
    // app.useGlobalPipes(new ValidationPipe()); - 管道
    // app.useGlobalFilters(new HttpExceptionFilter()) - 过滤器
	// app.useGlobalGuards(new AuthGuard()); - 守卫
    // app.useGlobalInterceptors(new LoggingInterceptor()); - 拦截器
    await app.listen(4002, '0.0.0.0'); // 设置后端项目的端口号为4002,监听所有可用网络接口
}
bootstrap();

2、根模块 app.module.ts

// 根模块 app.module.ts
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

@Module({
    imports: [CatsModule], // 导入模块的列表,在构造器/服务的构造函数中注入这些模型
})
export class ApplicationModule {} // 根模块
// 模块类(子模块) - cats/cats.module.ts
@Module({
    controllers: [CatsController], // 创建控制器【必须创建】,用于处理 HTTP 请求
    providers: [CatsService],      // 由 Nest 注入控制器实例化的 Provider,封装业务逻辑
    imports: [SequelizeModule.forFeature([Time, Routine])],
    							   // 将一个或多个 Sequelize 模型注册到当前模块中
    exports: [CatsService]         // 由本模块提供,并在其他模块中可用的 Provider -> 声明后,每个导入 CatsModule 的模块都可以访问 CatsService 
})
export class CatsModule {}

3、控制器 app.controller.ts

路由机制:装饰器 @Get @Post @Patch + 装饰器定义的控制器 @Controller(‘cats’)

  • 作用:哪个控制器接收哪些请求
  • 过程:按照定义的顺序进行,遍历所有的控制器 + 路由处理程序,找到第一个匹配
①作用

​ 负责处理「传入的请求」和向客户端「返回响应」 -> 控制器总是属于模块,所以要在 app.module.ts 中定义

② 数据传输对象 DTO

​ 定义如何通过网络发送数据,通过 TS 的 interface/简单的类来定义

  • 更推荐「类」,因为 ts 的定义在转换过程中被删除,Nest 不能在运行时运行
③ 写法:内部结构是「类+ 装饰器」

​ 每个控制器有多个路由,不同的路由可以执行不同的操作

// cats.controller.ts
@Controller('cats')
export class CatsController {
    constructor(private catsService: CatsService) {} // CatsService,通过类构造函数注入
    
    @Get('hello') // 路由路径
    getHello(@Query() query: CatGetHelloDTO): string {
        return 'This action returns all cats';
    }
}
④ 响应:两种不同的响应方式
  • 1、标准情况
    • return 一个 JS 「对象或者数组」时,Nest 会将其 自动序列化为 JSON
    • return 一个 JS 基本类型时,Nest只发送值,不尝试序列化它(案例如上代码)
  • 2、类库特有类型:例如 Express 使用 response.status(200).send() 构建
⑤ 请求:获取客户端请求
  • 1、@Req():Request(默认 Express)getHello(@Req() request: Request),指示 Nest 将请求对象注入处理程序 -> Request代表 HTTP 请求,有请求头、请求行、请求体等

  • 2、@Body() + @Query:专用装饰器,开箱即用

  • 3、@Param:动态路由参数 @Param() + 在路由路径中添加路由参数标记 @Get(':id')

      @Get(':id')
      findOne(@Param() params): string {
          // 写法2:findOne(@Param('id') id) - 按参数名称直接引用路由参数,可以直接使用 id
          return `This action returns a #${params.id} cat`;
      }
    
⑥ 其他装饰器
  • 状态码:响应的状态码一般都默认为 200,POST 默认为 201,想修改可以使用装饰器 @HttpCode(204)
  • 自定义Headers :@Header('Cache-Control', 'none')
  • 重定向:@Redirect('https://nestjs.com', 301)

4、基本服务 app.service.ts

① Providers 提供者

​ 很多基本类都被称为 Provider - 简单而言,就是一个用 @Injectable() 装饰器注释的类(还有很多其他方法)

// cats.service.ts
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
    private readonly cats: Cat[] = []; // private 将该参数转换为类的私有属性 + readonly 实例在控制器的生命周期内不会被意外修改
    create(cat: Cat) {
        this.cats.push(cat);
    }
    findAll(): Cat[] {
        return this.cats;
    }
}
② 作用域 - 生命周期

​ Provider 通常具有与应用程序生命周期同步的生命周期(“作用域”)。

  • 启动:启动应用程序时,必须解析每个依赖项,因此必须 实例化每个 Provider

  • 关闭:应用程序关闭时,每个 provider 都将被销毁

③ 注入方式
  • 1、基于构造函数的注入:app.controller.ts 中在类构造函数中注入 constructor(private catsService: CatsService) {}

  • 2、基于属性的注入:@Inject() 装饰器

      // cats.service.ts
      @Injectable()
      export class HttpService<T> {
          @Inject('HTTP_OPTIONS')
          private readonly httpClient: T;
      }
    
  • 3、提供者「服务」也可以注入到模块(类)「子模块」中

    • 但由于循环依赖性,模块类不能注入到提供者「服务」
    • 在子模块的构造函数中注入服务,在子模块内部即可使用服务,主要用于子模块初始化、配置等操作
      // cats.module.ts
      @Module({
          controllers: [CatsController],
          providers: [CatsService], // 服务 注入 控制器
      })
      export class CatsModule {
          constructor(private readonly catsService: CatsService) {} // 服务 注入 子模块
      }
    
  • 4、全局模块(类似helper、数据库连接等):@Global 装饰器 - 并不是一个好的解决方案

      // cats.module.ts
      @Global()
      @Module({
          controllers: [CatsController],
          providers: [CatsService],
          exports: [CatsService], // CatsService 将无处不在,不需要在别的模块中 imports 中导入
      })
      export class CatsModule {}
    

二、中间件

中间件是在路由处理程序 之前 调用的函数

  • 时机解释:中间件是基于底层的 Express 实现的,在请求进入 Nestjs 的路由系统之前就会被执行

  • 缺点:中间件不知道执行上下文
    • 所以参数验证交给 管道 来做,中间件不知道被调用的处理程序及其参数
  • 使用场景:日志记录、身份验证、请求解析,用于将控制权传递给下一个中间件或路由处理程序

可以访问原生的 请求 request + 响应 response + 请求响应周期中的 next 函数(中间件函数)

1、实现方式(2种)

①函数式中间件 - Function
export function logger(req, res, next) {
    console.log(`Request...`);
    next();
};
②在@Injectable()的类中声明 - NestMiddleware/use
  • implements 用于类实现「接口」,表明类会遵守这个契约 - 即实现接口中定义的所有成员
// logger.middleware.ts - NestMiddleware/use
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction) {
        console.log(req, res);
        next();
    }
}

2、配置方式

中间件不能在 @Module() 装饰器中列出,因为与 provider 功能和设计目的不同,所以不像 provider 声明形式

①模块配置 - configure
  • 必须使用模块类的 configure() 方法来设置它们(在模块内有更灵活的应用范围和执行时机)
// app.module.ts - NestModule/configure
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
    imports: [CatsModule],
})
export class AppModule implements NestModule { // 包含中间件的模块必须实现 NestModule 接口
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(LoggerMiddleware, logger) 
            .forRoutes(CatsController, Cats2Controller);
        	// 将中间件们绑定到当前模块所有路由 + forRoutes 声明的控制器的所有路由
    }
}

补充:MiddlewareConsumer 是一个帮助类,内置了几种方法管理中间件

  • apply: 可以使用单个中间件,也可以使用多个参数来指定多个中间件

  • exclude:排除某些路由

      consumer
        .apply(LoggerMiddleware) // 将中间件绑定到当前模块的所有路由 + forRoutes 声明的控制器中的所有路由,除了 exclude 声明的路由
        .exclude(
          { path: 'cats', method: RequestMethod.GET },
          { path: 'cats', method: RequestMethod.POST },
          'cats/(.*)',
        )
        .forRoutes(CatsController, Cats2Controller);
    
  • forRoutes:传递字符串、多个字符串、对象、控制类

      .forRoutes('cats')
      .forRoutes(
          { path: 'ca*ts', method: RequestMethod.GET, controller: CatsController }
      )
      .forRoutes(
          { path: 'app', method: RequestMethod.GET, controller: AppController },
          { path: 'user', method: RequestMethod.POST, controller: UserController },
      )
      .forRoutes(CatsController)
      .forRoutes(CatsController, Cats2Controller)
    
②全局中间件:app.use

一次性绑定到每个注册路由, 所有请求进行处理

// main.ts
const app = await NestFactory.create(AppModule);
app.use(LoggerMiddleware); // 常用于实现一些通用的功能,如日志记录、请求验证、性能监控等。
await app.listen(3000);

三、异常过滤器

1、异常类的定义

  • 内置基础异常类:HttpException

      // cats.controller.ts
      @Get()
      async findAll() {
          throw new HttpException({
              status: HttpStatus.FORBIDDEN,
              error: 'This is a custom message',
          }, HttpStatus.FORBIDDEN);
      }
      // 也可以直接传递一个 string
      throw new HttpException('Forbidden message info', HttpStatus.FORBIDDEN)
    
  • 自定义异常类(从基础类继承)

    • Nest 提供了一系列继承自核心异常 HttpException 的可用异常(很多)
      // forbidden.exception.ts
      export class ForbiddenException extends HttpException {
          constructor() {
              super('Forbidden', HttpStatus.FORBIDDEN);
          }
      } //  可以直接在 控制器 的方法中 throw new ForbiddenException()
    

2、异常过滤器

对异常层拥有完全控制权:捕获 HttpException 类实例的异常,自定义响应逻辑

两种定义方法
  • ① 自定义响应逻辑 - implements ExceptionFilter
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter{}
  • ② 通过继承定义:将异常处理委托给基础过滤器 - extends BaseExceptionFilter
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter{}

补充:ArgumentsHost 辅助方法 - 需要访问底层的 Request 和 Response

  • 使用 ArgumentsHost 来获取,是因为它可以在所有上下文中使用,包括 HTTP 服务器上下文、微服务、Websocket 等
绑定方法 - 根据范围区分绑定方式
  • ①方法范围(为某个路由设置)、控制器范围(为每个路由设置) - 装饰器 @UseFilters

      @UseFilters(new HttpExceptionFilter())
    
  • ②全局范围(为每个路由、每个控制器、整个应用程序设置)

      // 方法一:在 main.ts 配置
      app.useGlobalFilters(new HttpExceptionFilter())
    
      // 方法二:注入到全局 provider + useClass ,在 app.module.ts 配置
      import { Module } from '@nestjs/common';
      import { APP_FILTER } from '@nestjs/core';
      // 后文管道 依赖注入 用法相似 import { APP_PIPE } from '@nestjs/core';
      // 后文守卫 依赖注入 用法相似 import { APP_GUARD } from '@nestjs/core';
      // 后文拦截器 依赖注入       import { APP_INTERCEPTOR } from '@nestjs/core';
      @Module({
          providers: [
              {
                  provide: APP_FILTER, // 令牌
                  useClass: HttpExceptionFilter, // 要实例化的类
              },
              {
                  provide: APP_PIPE,
                  useClass: ValidationPipe
              },
              {
                  provide: APP_GUARD,
                  useClass: RolesGuard,
              },
              {
                  provide: APP_INTERCEPTOR,
                  useClass: LoggingInterceptor,
              },
          ],
      })
      export class AppModule {}
    

四、管道 Pipe

管道:是具有 @Injectable() 的类 + 实现了 PipeTransform 接口

1、应用场景

转换 + 验证(例如验证路由参数、查询字符串参数、请求体格式等)

2、时机

Nest 调用 控制器的路由处理程序 之前,插入一个管道 -> 处理输入参数 -> 再用参数调用原方法

  • 一般来说,中间件会早于管道执行(中间件基于底层的 Express 实现,管道是 Nestjs 框架层面的概念)
    • 中间件 - 在请求进入 NestJS 的路由系统之前就会被执行
    • 管道 - 请求 已经进入到 NestJS 的路由系统,并且已经确定了要调用的控制器、处理程序之后处理
3、定义:(内置管道 + 自定义管道) - PipeTransform

① 内置管道:开箱即用(9个 - 中间6个被称为 Parse* 管道)ValidationPipe、ParseIntPipe、ParseFloatPipe、ParseBoolPipe、ParseArrayPipe、ParseUUIDPipe、ParseEnumPipe、DefaultValuePipeParseFilePipe

②自定义管道 PipeTransform

  • PipeTransform<T, R> 是每个管道必须要实现的泛型接口(泛型 T 表明输入的 value 的类型,R 表明 transfrom() 方法的返回类型)
// 转换管道 简单的 ParseIntPipe 实现 - 将字符串转换为整数 parse-int.pipe.ts 
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
    transform(value: string, metadata: ArgumentMetadata): number { // 必须声明 transform
        const val = parseInt(value, 10); // value - 需要被转换为整数的原始值(转换前的数据)
        if (isNaN(val)) {
            throw new BadRequestException('Validation failed');
        }
        return val; // 返回的值完全覆盖了参数先前的值
    }
}
// 验证管道 写法1:使用 ObjectSchema 的 validate 来实现验证
import { ObjectSchema } from 'joi'; // 安装了 joi 和 @types/joi 
@Injectable()
export class JoiValidationPipe implements PipeTransform {
    constructor(private schema: ObjectSchema) {}
    transform(value: any, metadata: ArgumentMetadata) {
        const { error } = this.schema.validate(value); // 实现验证
        if (error) {
            throw new BadRequestException('Validation failed');
        }
        return value;
    }
}
// 验证管道 写法2:cat.dto.ts 使用 class-validator 生成 CatDto
import { IsString, IsInt } from 'class-validator'; // 安装 class-validator class-transformer
export class CatDto {
    @IsString()
    name: string;

    @IsInt()
    age: number;

    @IsString()
    breed: string;
}
// validate.pipe.ts 利用 class-transformer 创建 ValidationPipe 类(太复杂,只理解,看[官网](https://docs.nestjs.cn/10/pipes))
4、绑定 - 根据管道类型不同,绑定方式不同

①转换管道 - 作为参数传入装饰器 @Param @Query

  • 若无法转换,在路由处理程序被调用前就会在管道里报出异常,阻止了路由处理程序的调用
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) { 
    // 写法2:除了传递 类,还可以传递 实例 @Param('id', new ParseIntPipe({...}))
    return this.catsService.findOne(id);
}

补充:Parse* 管道期望参数值是被定义的,当接收到 null 或者 undefined 值时,它们会抛出异常

  • 为了允许 null/undefined,对这些值进行操作之前注入默认值 -> 只需要实例化 DefaultValuePipe
// DefaultValuePipe 的使用
@Get()
async findAll(
    @Query('sunny', new DefaultValuePipe(false), ParseBoolPipe) sunny: boolean,
    @Query('sort', new DefaultValuePipe(0), ParseIntPipe) sort: number,
    ) {
        return this.catsService.findAll({ sunny, sort });
    }

②验证管道

  • 提供了用于在系统边界「验证」从外部源进入应用程序的数据的一种 最佳实践

  • 绑定范围:参数范围 @Body、方法范围 @UsePipes、控制器范围、全局范围 useGlobalPipes

// [方法范围] UsePipes
@Post()
@UsePipes(new CatsPipe(catsSchema))
async create(@Body() catDto: CatDto) {
    this.catsService.create(catDto);
}
// [参数范围] 检验 post body - @Body
@Post()
async create(@Body(new CatsPipe()) ca	tDto: CatDto ) {
    this.catsService.create(catDto);
}
// [全局范围] main.ts - useGlobalPipes / useClass
useGlobalPipes(new CatsPipe());

五、守卫

  • 定义:一个使用 @Injectable() 装饰器的类 + 实现 CanActivate 接口
  • 时机:在 每个中间件 之后,但在拦截器 或者 管道之前执行

  • 作用:实现一个责任/授权/认证,根据运行时出现的某些条件(例如权限,角色,访问控制列表等)来确定给定的请求是否由路由处理程序处理
    • 在传统的 Express 中一般用中间件处理
    • 和中间件差异:中间件不知道调用 next() 函数后会执行哪个处理程序;守卫可以访问 ExecutionContext 实例,因此确切地知道接下来要执行什么
  • 绑定:可控制范围(控制器范围、方法范围:@UseGuards(类/实例)装饰器、全局范围:useGlobalGuards/useClass)
授权守卫实现 - CanActivate
  • 只有当调用者(通常是经过身份验证的特定用户)具有足够的权限时,特定的路由才可用
    • canActivate 函数返回一个布尔值:true - 允许当前请求;false - Nest 忽略当前处理请求
// auth.guard.ts - 提取和验证请求头的token,并使用提取的信息来确定请求是否可以继续
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
    canActivate( // 必须实现一个 canActivate
    	context: ExecutionContext 
    ): boolean | Promise<boolean> | Observable<boolean> { 
        const request = context.switchToHttp().getRequest();
        return validateRequest(request); // 自定义 validateRequest 函数
    }
}

补充:ExecutionContext 继承并扩展自 ArgumentsHost 辅助方法 - 可以访问底层的 Request 和 Response

角色认证守卫
  • 只允许具有特定角色的用户访问

    • 守卫返回 false 时,框架会抛出一个 ForbiddenException 异常(默认由异常层处理)
    • 补充 Reflector 帮助类
      // roles.guard.ts
      import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
      import { Reflector } from '@nestjs/core';
        
      @Injectable()
      export class RolesGuard implements CanActivate {
          constructor(private reflector: Reflector) {}
          canActivate(context: ExecutionContext): boolean {
              const roles = this.reflector.get<string[]>('roles', context.getHandler());
              if (!roles) {
                  return true;
              }
              const request = context.switchToHttp().getRequest();
              const user = request.user; // 包含用户实例和允许的角色
              return matchRoles(roles, user.roles); // 自定义
          }
      }
    
  • 补充 @SetMetadata() 装饰器 - 将定制元数据(角色数据)附加到路由处理程序

// 自定义装饰器 @Roles roles.decorator.ts 
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// roles 元数据(roles 是一个键,而 ['admin'] 是一个特定的值)
// cats.controller.ts
@Post()
@Roles('admin') // 等价于 @SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
}

六、拦截器

  • 定义:一个使用 @Injectable() 装饰器的类 + 实现 NestInterceptor 接口

  • 拦截器功能【可重用解决方案】
    • 在函数执行之前/之后绑定额外的逻辑
    • 转换从函数返回的结果
    • 转换从函数抛出的异常
    • 扩展基本函数行为
    • 根据所选条件完全重写函数 (例如, 缓存目的、缓存拦截器)
  • 绑定:@UseInterceptors(类/实例) 装饰器 、useGlobalInterceptors()/useClass
案例:使用拦截器在函数执行之前或之后添加额外的逻辑
  • 记录与应用程序的交互:存储用户调用、异步调度事件、计算时间戳

补充:CallHandler - 是一个包装执行流的对象 -> Nest订阅了返回的流,并使用此流生成的值来为最终用户创建单个响应或多个响应 -> 所以说:CallHandler 推迟了最终的处理程序执行

// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { tap, map, catchError, timeout } from 'rxjs/operators';
@Injectable()
export class Interceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> { // 必须实现 intercept,如果不手动调用 handle() 方法,主处理程序根本不会进行求值
        console.log('Before...');
        const now = Date.now();
        return next
            .handle() // 返回一个 RxJS Observable 流,包含从路由处理程序返回的值
            .pipe(
            	timeout(5000), // 5秒后,请求处理将被取消
            	tap(() => console.log(`After... ${Date.now() - now}ms`)), // tap 运算符 - 在可观察序列的正常或异常终止时调用函数
            	map(value => value === null ? '' : value ), // 将每个null值转换为空字符串 ''
            	catchError(err => throwError(new BadGatewayException())), // 覆盖抛出的异常
        	);
    }
}

七、自定义装饰器

Nest 是基于装饰器这种语言特性而创建的

  • 装饰器:一个表达式,它返回一个可以将目标、名称和属性描述符作为参数的函数 @装饰器 为类、方法、属性定义

Nest 提供参数装饰器:

@Request()/@Req()-req@Response()/@Res()-res@Next()-next
@Session()-req.session@Ip()-req.ip@HostParam()-req.hosts
@Param(param?: string)   -req.params / req.params[param]
@Body(param?: string)    -req.body   / req.body[param]
@Query(param?: string)   -req.query  / req.query[param]
@Headers(param?: string) -req.headers/ req.headers[param]
自定义装饰器 - createParamDecorator
// user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator((data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user; // 在每个路由处理程序中手动提取 user
    return data ? user && user[data] : user;
});
//  @User(参数也可以是管道)
@Get()
async findOne(@User('firstName') firstName: string) {
    console.log(firstName);
}
聚合装饰器 - applyDecorators
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles: Role[]) {
    return applyDecorators(
        SetMetadata('roles', roles),
        UseGuards(AuthGuard, RolesGuard),
        ApiBearerAuth(),
        ApiUnauthorizedResponse({ description: 'Unauthorized"' })
    );
}
@Get('users')
@Auth('admin') // 通过一个声明应用所有四个装饰器
findAllUsers() {}