Nestjs 核心特性、基本概念、生命周期、获取请求和响应、不同组件的实现和执行顺序
总结:
- 核心特性:模块化架构 + 依赖注入DI + 多种协议 HTTP/websocket
- 基本概念:控制器 controller + 根模块/子模块 module + 服务 provide + 应用程序入口 main.ts
- 在模块中配置,将服务注入控制器
- 令牌:类是代码的具体实现,令牌是依赖注入的 用于标识依赖项的唯一标识,通过令牌请求相应实例
- 装饰器:一个表达式,返回一个函数,参数为目标、名称、属性描述符
createParamDecorator
- Nest 生命周期:
- ①应用初始化:NestJS 框架启动,解析模块、控制器、provider 等配置信息[通过装饰器@Module @Controller @Injectable 定义的一些元数据,定义了各个组件],构建依赖[收集、整理组件间的依赖关系]注入容器
- 容器:框架内部用于管理组件和依赖关系的一个抽象概念,应用启动时创建
- ②依赖解析和实例化:根据配置,解析各组件的依赖关系[深入分析],根据关系依次实例化 provider、实例化控制器
- ③应用启动:所有组件实例化后,应用监听指定端口,正式提供服务
- ④请求处理:运行过程中,接收请求,按照中间件-守卫-拦截器-管道-拦截器顺序处理请求,返回响应
- ⑤应用关闭:执行关闭操作,销毁各组件实例
- ①应用初始化:NestJS 框架启动,解析模块、控制器、provider 等配置信息[通过装饰器@Module @Controller @Injectable 定义的一些元数据,定义了各个组件],构建依赖[收集、整理组件间的依赖关系]注入容器
- 不同组件的生命周期:
- 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()
- Nest 提供了一系列继承自核心异常
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、DefaultValuePipe
、ParseFilePipe
②自定义管道 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() {}