NestJS
基本介绍
一个构建高效的,可扩展的NodeJS开发框架,利用JS的渐进式增强的能力,使用并支持TS(仍然允许使用JS)并结合了OPP(面向对象编程)FP(函数式编程)和FRP(函数响应式编程)
底层,建立在强大的HTTP服务器框架上面,还可以通过配置使用Fastify(也是一个NodeJS框架,专注提供最快的HTTP服务,性能接近NodeJS原生)
强语言类型,类似于Java spring
创建项目
sql
pnpm add @nestjs/cli -g
nest new <project-name>
创建出来文件之后的结构
app.controller.ts
带有路由单元的基本控制器
app.controller.spec.ts
针对控制器的单元测试
app.module.ts
T应用程序的根模块
app.service.ts
具有单一方法的基本服务
平台:
支持多平台(框架)使用,nest可以运行在不同的底层HTTP平台之上,无需重写业务逻辑
NestJS本身就是一个抽象框架,不直接处理HTTP请求/响应,而是通过一个平台适配层和底层的HTTP引擎解耦,目前支持express fastify使用
很多的nest功能在两个平台上面都支持使用
但是 express中间件和fastify插件不支持直接使用,需要和适配插件结合使用
主要分成三层 controller service module
controller:核心作用就是接收用户的HTTP/GraphQl/RPC请求,定义路由规则,最后返回响应之类的
- 主要职责:定义路由,接收参数,调用service,返回响应
service:处理业务的逻辑层
- 核心作用:封装具体的业务逻辑,(数据处理,计算,调用数据库之类的)
- 主要职责:封装业务逻辑,操作数据,处理复杂逻辑
module:组织代码的容器层
- 核心作用:将相关的controller, sevice依赖等组织在一起,定义边界和依赖关系,基本组织单元
- 主要职责:声明组件,管理依赖,封装功能
Controller 控制器
基础支持
控制器的目的就是接收应用程序特定请求,路由机制控制哪个控制器接收哪个请求(具体的逻辑通常是放在service里面的),通常,每个控制器都有多个路由,不同的路由可以执行不同的操作
装饰器:装饰器本质上就是一个函数,可以用来修改类,方法,属性或者参数,在NestJS当中,装饰器通过@符号来使用,后面紧跟装饰器的名称,@controller @Get
等
举例
kotlin
import { Controller } from '@nestjs/common';
@Controller('users')
export class UsersController {
// 控制器中的方法定义
}
意思就是将这个类是和/user这个路径请求的控制器
@Get() @Post()
方法装饰器,用来标识实际的请求,可以使用装饰器对一类的请求进行合并集成,和exoress使用类方法对方法进行定义差不多
底层原理:实际上是通过反射机制来读取和修改类,方法,属性或者参数的元数据,TS会在编译阶段将装饰器转化成对应的JS代码,这些代码运行时候会对目标对象进行操作
路由响应
有两种不同的选项来操作响应的概念
-
Stand 标准
- 使用这个方法的时候,如果请求程序返回JS对象或者数组吗,会自动序列化成JSON,但是Nest只会发送值,并不会尝试序列化数据
-
Library-specific
- 我们可以使用特定的库响应对象,可以使用这个方法处理程序签名中的
@Res()
装饰器注入,(eg:findAll(@Res() response)
)通过这种方法,可以使用这个对象公开本机响应处理方法 下面就是使用express的形式
less@Post() postHello(@Res() response: Response): any { response.send('123'); }
- 我们可以使用特定的库响应对象,可以使用这个方法处理程序签名中的
路由通配符
类似于正则,但是和正则又不相同,可以查看文档了解一下
状态码
当我们想要设置返回的状态码时候,我们就可以使用@HttpCode
less
@Get() // 请求方式 restful
@HttpCode
getHello(): string {
return this.appService.getHello()
}
请求头
使用@header()
设置对应的请求头
重定向
@Redirect
路由参数
params
less
@Get(':id')
findOne(@Param() params: any): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
@Get(':id')
findOne(@Param('id') params: any): string {
return `This action returns a #${id} cat`;
}
query
less
@Get(':id')
findOne(@query() query: any): string {
console.log(query.id);
return `This action returns a #${query.id} cat`;
}
子域路由
就比如我们使用三级路由才能匹配到这个路由执行函数,使用二级路由就匹配不到
kotlin
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
Scopes
Asynchoricity
异步执行函数,支持异步操作
请求负载
我们需要确定DTO(数据传输对象)模式,DTO是一个对象,定义了网络发送什么数据,可以通过TS接口或者简单的类来确定DTO的模式,用来body传值
tips:为什么推荐使用类:类是ES6标准的一部分,因此运行的时候是作为实体保存的,但是TS的接口在编译过程中是会被删除的,Nest运行的时候无法使用,
例如:
less
import { IsInt, IsString } from "class-validator"
export class CreateCatDto {
@IsString()
name: string
@IsInt()
age: number
@IsString()
breed: string
}
请求接口的时候 记得使用验证方法进行验证
less
@Post()
@UsePipes(new ValidationPipe()) // 管道验证
async create(@Body() createCatDto: CreateCatDto) {
console.log(createCatDto)
return 'This a action add a new cat'
}
错误处理
抛出错误 类似于Java 使用throw关键词就可以抛出错误
开始运行
将定义的controller和Service放置到module当中
Providers 提供者
service
Nest当中的重要概念,许多基本的Nest类可以被视为提供者-服务,仓库,工厂,助手等,提供者的主要思想就是可以作为依赖注入,这就意味着对象可以创建彼此之间的关系,并且链接这些对象的功能,可以在很大的程度上委托给Nest运行时系统
主要是通过Injectable
实现的
依赖注入
Nest的依赖注入(DI)系统和管道自动完成创建的,无需手动的进行创建
作用流程:通过依赖注入系统自动管理控制器,服务等核心组件的实例创建和依赖注入,通过管道自动处理DTO的实例化和验证,开发者不需要手动的进行new,只需要通过装饰器声明类的角色即可
typescript
constructor(private catsService: CatsService) {}
这个是基于构造函数注入,提供者提供构造函数的方法注入
属性注入
有些情况下属性注入会更有优势:当顶级类依赖一个或者多个提供者的时候,将他们一路透传到子类中会非常的繁琐,可以在属性级别当中使用@Inject()
装饰器使用
kotlin
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
Scopes
Custom provides
OptionsProvider可选
我们可能认为有些不是强依赖,例如我们的类可能需要配置某些依赖项,但是如果没有提供这个对象,我们有默认值兜底,这样的情况,我们可以使用@Options
表示提供者是可以选择的
less
import { Inject, Injectable, Optional } from '@nestjs/common';
import { CatsService }'../cats/cats.service'
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
/*
* 基于构造函数的注入也一样
* constructor(@Optional() private catsService: CatsService) {}
* 此时,就算 CatsService 没有在 App.module 中注册提供者,也不会报错
*/
}
Module 模块
使用@module
装饰器注释的类,装饰器提供Nest用来组织应用程序结构的元数据
使用module能够让我们的代码进行分整,我们可以将不同的模块进行分离,每个模块管理一个特定的功能
module能够接收一个包含模块的对象
providers |
Nest 注入器将实例化的提供者,可以至少在本模块中共享 |
---|---|
controllers |
本模块中定义的控制器集合,需要被实例化 |
imports |
导入的模块列表,这些模块都导出了本模块需要的提供者 |
exports |
providers 的子集,这些提供者由本模块提供,并且可以在其他导入了本模块的模块中使用。可以使用提供者本身,也可以仅使用他的token 值(provide value ) |
Feture Module
将密切相关的service和controller层属于同一个领域,我们将他们放到同一个特性模块是合理的,特性模块组织了和特定功能相关的代码,有助于维护清晰的边界和更好的组织
对这个module 在main module进行注册(import)之后,service和controller就可以不再注册
Shared Module
Nest当中,默认情况下是单例的,所以可以轻易的在多个模块当中之间共享一个模块实例
每个模块创建之后,默认(自动)就是一个共享模块,可以被其他的任何模块重用,其他模块想要使用这个模块实例的时候,我们需要将他添加到export数组中进行导出
减少内存消耗,允许在整个应用程序当中高效共享
Module Re-exporting
就是导进去的模块,可以选择再次导出
kotlin
@Module({
imports: [CommonModule],
exports: [CommonMoudle]
})
export class CoreModule {}
DI 依赖注入
模块类也可以注入提供者(用于配置目的)
typescript
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private catsService: CatsService) {}
}
Global module
使用@Global装饰器进行作用,这样全局使用的模块就可以不再需要一致引入了
python
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsSetivce]
})
export class CatsModule {}
Dynamic Module
这个主要是实现运行时配置的参数,他们是不符合要求的,动态模块就允许我们在运行的时候进行配置模块,允许被导入的时候接收参数,并根据这些参数动态创建provider exports等的内容,从而实现内容的定制化复用,
typescript
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';
@Module({
providers: [Connection],
exports: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
// 使用方法
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database.module';
import { User } from './entities/user.entity';
import { Product } from './entities/product.entity';
@Module({
imports: [
DatabaseModule.forRoot(
[User, Product], // 注册需要的实体
{ host: 'localhost', port: 5432 }, // 数据库配置
),
],
})
export class AppModule {}
使用动态模块,增加灵活性,可以根据不同的参数生成不同的配置,提高代码的复用性,避免重复开发
Middleware 中间件
基础知识
中间件也需要被注入到其他的类当中
- 中间件需要使用
@Injectable()
装饰器执行,表明可注入 - 中间件需要实现
NestMiddleware
接口,(实现use函数,传入req,res,next参数)
多个中间件的执行,只需要使用apply()
中间件功能执行一下任务:
- 执行任何代码
- 对请求和响应对象进行更改
- 结束请求,相应周期
- 调用堆栈中的下一个中间件函数
- 如果中间件没有结束请求,就必须调用next将控制传递到下一个中间件函数
中间件消费者
MiddlewareConsumer
是一个辅助类,提供了多种内置方法,管理中间件,所有的方法都支持链式调用,forRoutes方法可以接收多个字符串,一个RouteInfo
对象,控制器类,或者逗号分的多个控制器类
排除路由
不想应用某些中间件的路由 excusive()
进行排除
函数中间件
当中间件实现非常简单,没有成员,没有方法,没有依赖,我们就可以做成函数中间件的形式
javascript
import { NextFunction, Request, Response } from 'express';
export function demo(req: Request, res: Response, next: NextFunction) {
console.log('demo');
next();
}
// app中正常使用即可
consumer
.apply(demo)
.forRoutes(CatsController);
多个中间件
应用中间件的顺序就是执行顺序,用逗号隔开
全局中间件
创建的Nest实例对象的下面使用use方法,可以将所有的中间件应用到所有的路由当中
Exception Filters异常过滤器
Filter的唯一作用就是管理所有异常,专注于错误处理,让错误处理,变得更加规范,灵活
默认情况下,异常处理操作由内置的全局异常过滤器执行,这个过滤器处理类型为HttpException
及其子类的异常,不是这类异常就会返回 500
如果是404一类的消息就会返回对应的默认信息
lua
status ... message...
返回标准的异常
arduino
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)
内置异常
diff
- BadRequestException
- UnauthorizedException
- NotFoundException
- ForbiddenException
- NotAcceptableException
- RequestTimeoutException
- ConflictException
- GoneException
- HttpVersionNotSupportedException
- PayloadTooLargeException
- UnsupportedMediaTypeException
- UnprocessableEntityException
- InternalServerErrorException
- NotImplementedException
- ImATeapotException
- MethodNotAllowedException
- BadGatewayException
- ServiceUnavailableException
- GatewayTimeoutException
- PreconditionFailedException
自定义异常过滤器
主要就是三步:定义类,实现接口,注册使用
如何使用过滤器
-
全局使用
- 直接在app上面挂载或者模块里面全局注册
-
局部使用
-
想要让某个控制器,或者控制器下的某个接口使用filter,就可以使用
@useFilter()
装饰器typescriptimport { Body, Controller, Get, HttpException, Optional, Post, UseFilters, } from '@nestjs/common'; import { CreateCatDto } from './dto/create-cat.dto'; import { CatsService } from './cats.service'; import { Cat } from './interface/cat.interface'; import { HttpService } from 'src/http/http.service'; import { HttpExceptionFilter } from 'src/filters/http.filter'; @Controller('cats') @UseFilters(HttpExceptionFilter) // 单个控制器局部使用,影响所有接口 export class CatsController { constructor( private catsService: CatsService, @Optional() optionService: HttpService<string>, ) {} @UseFilters(HttpExceptionFilter) async findAll(): Promise<Cat[]> { return this.catsService.findAll(); } @Post() @UseFilters(HttpExceptionFilter) // 单个接口局部使用(这里仅作演示) async add(@Body() createCatDto: CreateCatDto) { if (!createCatDto?.name || createCatDto.name === '') { throw new HttpException('必须包含「姓名」', 400); // 会被专门处理 HttpException 的 Filter 捕获 } return this.catsService.add(createCatDto); } }
-
Pipes 管道
基本内容
Pipe是专门处理数据转换和验证的组件
Pipe是专门打理数据的小助手,例如当前端传递的信息不正确的时候,Pipe能自动转化成正确的格式,减少报错
简单的说,核心功能就是两个数据转换和数据验证,执行实际在控制器接口方法之前(参数先传递给Pipe,没问题再次传递给接口)
核心作用
数据验证 validation
常用的场景:检查前端传递的参数是否符合要求,(比如必填,格式正确,长度达标)不符合直接报错,不用接口自己判断
例如:当我们需要一个用户年龄参数的时候,不适用pipe方案,我们可能需要这样写
typescript
@Post('/user')
createUser(@Body() body: any) {
const { age } = body;
// 1. 检查 age 是否存在
if (!age) {
throw new Error('必须包含「年龄」');
}
// 2. 检查 age 是否是数字
if (typeof age !== 'number') {
throw new Error('年龄必须是数字');
}
// 检查 age 范围
if (age < 0 || age > 60) {
throw new Error('年龄必须在 0 到 60 之间');
}
// 业务逻辑
return {
message: '创建用户成功',
success: true,
data: body,
};
}
但是使用Pipe我们就不需要关心数据的验证,只需要Pipe进行管理
数据转换 transform
'1221' =》 1221
内置pipe
Nest内置了六个常用的Pipe,不用写自己就能使用,覆盖大部分场景
名称 | 作用 | 使用场景 |
---|---|---|
ValidationPipe |
最强大的 Pipe,既能验证又能转换,支持类验证器(比如@IsNumber() ) |
验证请求体、查询参数、路径参数(复杂结构) |
ParseIntPipe |
把参数转换成数字,转不成就报错 | 路径参数/查询参数需要数字(id ,页码) |
ParseFloatPipe |
和 ParseIntPipe 类似,转成浮点数 |
需要小数的场景,比如价格、评分 |
ParseBoolPipe |
把参数转换成布尔值 | 需要布尔值的场景,比如是否显示、是否启用 |
ParseArrayPipe |
把参数转换成数字,支持指定数组元素类型 | 需要数组的场景,比如批量查询 ID 列表 ?ids=1,2,3 |
DefaultValuePipe |
给可选参数设默认值(没传参数时用默认值) | 可选的查询参数,比如页码默认为1 ,每页默认10 tiao |
ValidationPipe重点解析
最核心的内置的pipe,需要配合类验证器(class-validator)和类转换器(class-transformer)能实现复杂的结构化数据验证
自定义Pipe
内置的Pipe不能满足业务的需求,就需要使用写自定义Pipe,分两步实现类,实现接口
核心就是实现transfor接口和transform方法,这两个方法是Pipe的核心逻辑
自定义Pipe核心规则
-
必须使用
@injectable
装饰器自定义Pipe -
必须实现PipeTransform接口,并重写transform方法
-
trasform参数的两个方法,有两个参数
- value 当前处理的参数值
- metadata 参数的元数据
-
tranform方法的返回值,会被传递给接口方法使用
注册方式
Pipe写好之后,需要告诉Nest什么时候受用,由局部使用,控制器级别,全局使用三种方式,优先级 局部>控制器>全局
局部注册-参数级别
最常用的方法给单个参数使用pipe,直接将Pipe穿在@body @Params @Query等装饰器里面
控制器级别注册
给整个控制器的所有接口都是用pipe,使用@UsePipes装饰器加载控制器类上
less
import { Controller, UsePipes, ValidationPipe } from '@nestjs/common';
// 控制器级别注册:所有接口都用 ValidationPipe
@Controller('user')
@UsePipes(new ValidationPipe({ whitelist: true }))
export class UserController {
@Post()
// 这里不用再传 ValidationPipe 了,控制器级别的会生效
create(@Body() createUserDto: CreateUserDto) {
return { success: true, data: createUserDto };
}
@Put(':id')
// 这个接口的 @Body() 也会用控制器级别的 ValidationPipe
update(@Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto) {
return { success: true, data: { id, ...updateUserDto } };
}
}
全局注册
所有的接口都使用Pipe,不用每个控制器都加UserPipes,尽量不要在全局注册,因为这样不会进行依赖注入(Pipe拿不到组件的实例对象,无法进行判断)
Pipe元数据
属性名 | 类型 | 含义 |
---|---|---|
type |
'body'/'param'/'query'/'custom' |
参数的来源类型(是请求体、路径参数、查询参数还是自定义) |
data |
strinng / undefined |
参数名(如果装饰器里传了参数名,比如 @Body('username') ,这里就是 'username' ;没传就是 undefined ) |
metatype |
Type<any> / undefined |
参数的类型(比如 String 、Number 或自定义 DTO 类) |
我们可以利用元数据执行差异化处理
比如根据参数的来源不同做出不同的处理
typescript
@Injectable()
export class CustomPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
switch (metadata.type) {
case 'body':
// 如果是请求体参数,转成小写(假设是字符串)
return typeof value === 'string' ? value.toLowerCase() : value;
case 'param':
// 如果是路径参数,转成数字
return typeof value === 'string' ? parseInt(value, 10) : value;
case 'query':
// 如果是查询参数,原样返回
return value;
default:
return value;
}
}
}
守卫 Guard
控制访问权限的组件,决定请求是否能到达路由处理程序
Guard:专门帮你守门的保安,对请求进行限制,进行权限判断
核心功能
基于角色权限控制 RBAC
Guard常用的场景就是RBAC,给用户分配不同的角色,在给不同的接口设置访问的角色请求,Guard负责核对访问角色要求,Guard负责对用户角色和接口是否匹配
比如,查看订单只有登录之后才能操作
自定义 Guard
Nest内部没有提供开箱即用的Guard,因为每个项目的权限规则,(角色定义,登陆状态不一样)就需要自己写自定义的Guard
自定Guard也是两步,定义类 实现接口
核心规则
-
必须使用
@injectable()
装饰器装饰类 -
必须实现
CanAvtive
接口,并重写canActive
方法 -
canActive
方法的逻辑- 接收
ExecutionContext
参数(能拿到当前请求的上下文,比如请求对象,路由信息) - 返回值是Boolean,或者Promise或者Observable
- 接收
-
可以注入其他服务,比如使用AuthService用来验Token,UserService用来查看用户状态
多个Guard执行顺序
给接口添加了多个Guard,会按顺序执行
Guard如何获取用户信息
实际项目当中用户信息是怎么进行获取的呢?
核心是通过请求上下文,获取请求对象,再从请求中解析用户信息,通常有两种方法
从Token中进行验证,前端传递 Authorization: Bearer xxx
头,Guard里面调用AuthService
验证Token,解析用户ID,查看数据库中用户的角色
从Session获取,使用session登录,用户信息会存储在request.session.user的里面,
注册方式
局部注册
less
// 整个 UserController 的所有接口,都需要先登录(AuthGuard)
@Controller('user')
@UseGuards(AuthGuard)
export class UserController {
// 这个接口继承了控制器的 AuthGuard,还额外加了 RoleGuard
@Get('/all-users')
@UseGuards(RoleGuard(['admin']))
getAllUsers() { /* ... */ }
// 这个接口只需要登录(继承控制器的 AuthGuard)
@Get('/profile')
getProfile() { /* ... */ }
}
全局注册
让所有的接口都经过某个Guard,比如全局登录验证,所有接口都需要登录才能访问
但是在app主程序中注册全局守卫但是不支持依赖注入,适合不需要i其他服务的Guard
或者就是在模块里面使用APP_GUARD进行标记,也可以全局生效
不需要Guard的进行排除
如果全局注册了Guard,比如所有的接口都需要登录,但是某些接口不用登录,比如的登陆,注册获取公开信息
就需要使用@public
这些自定义装饰器排除
创建@public
自定义装饰器
javascript
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
在Guard里面判断元数据
改造AuthorGiuard,如果接口里面有@public
装饰器,就直接放行
要在被公开的接口上面使用@public
进行装饰
less
import { Controller, Post, Get, UseGuards } from '@nestjs/common';
import { Public } from '../decorators/public.decorator';
@Controller('auth')
export class AuthController {
// 登录接口:公开,不需要登录(加 @Public())
@Post('/login')
@Public()
login() {
return { success: true, data: { token: 'xxx' } };
}
// 注册接口:公开
@Post('/register')
@Public()
register() {
return { success: true, data: '注册成功' };
}
// 刷新 Token 接口:需要登录(不加 @Public(),会走 Guard 验证)
@Get('/refresh-token')
refreshToken() {
return { success: true, data: { token: 'new-xxx' } };
}
}
拦截器 Interceptor
Interceptor是基于AOP(面向切换编程)思想,能拦截请求/响应数据,增强业务逻辑组件
简单的说,就是在正常响应流程中,插入自己操作的东西(包括但是不限于运行日志,执行时间,接口报错)
核心功能
-
转化请求数据
- 接口执行前,修改,加工客户端传递的参数
- 场景eg:下单的时候自动添加地址
- 代码eg: 前端{name:'111'} 转换数据 { name: '1111', createTime:23129312893}
-
转换响应数据
- 接口执行后,统一修改返回给客户端的格式
- 场景eg:所有订单返回时,自动加上订单描述状态
- 代码eg:接口返回id,status,拦截之后修改成success data 的形式
-
执行额外的逻辑:
- 接口执行前后,加一些额外的操作(日志,计时)
- 场景eg:下单之后自动记录操作日志
- 代码eg:接口执行前记录开始时间,执行之后计算耗时,打印日志
-
扩展错误处理:
- 接口报错时候,做额外的处理(重试,自定义处理之类的)
- 场景eg:库存不足的时候,自动重试一次查询
- 代码eg:接口抛出库存不足,拦截器重试之后仍失败再次返回错误
-
控制响应流
- 主动修改响应的返回时机(延迟返回,缓存响应)
- 场景eg:热门商品详情接口,缓存10min避免重复查库
- 代码eg:第一次请求库并缓存,10min内其他的请求直接返回缓存数据
核心原理:Observable流
Nest的Interceptori是基于RxJs,核心是通过Obsrever来拦截错误流程
接口执行的结果会包装成一个数据流(Observale),interceptor,可以拦截这个流,经过加工之后再次传递下去
核心方法是intercept
,接收两个参数
- context:执行上下文,拿到请求/响应对象
- next:下一个步骤的引用,调用nexthandle,就能触发接口执行,返回的是Observer流
- 返回值:加工之后的observeable流,必须返回流,否则客户端拿不到响应
自定义拦截器
自定义拦截器Intercetor就两步 定义类,实现接口,核心就是重写intecept方法,使用RxJs操作符加工Observable流
下面按照能力分场景书写例子:
scene1:统一相应格式,转换响应数据
最常用的场景就是所有的接口返回之后自动加上success和data字段,避免每个接口都写重复的代码
简单实例
定义拦截器
typescript
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';
// 定义响应的统一接口
interface Response<T> {
success: boolean;
data: T;
timestamp: number;
}
@Injectable()
export class TransformResponseInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
// intercept 方法: 核心逻辑
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<Response<T>> | Promise<Observable<Response<T>>> {
// 1. 调用 next.handle() 触发接口执行,拿到接口返回的 Observable 流
return next.handle().pipe(
// 2. 用 map 操作符修改流里的数据
map((data) => ({
success: true, // 固定返回 success: true,失败的情况交给 Filter 处理
data, // 原始数据
timestamp: Date.now(), // 加个时间戳
})),
);
}
}
使用拦截器
kotlin
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformResponseInterceptor } from '../interceptors/transform-response.interceptor';
@Controller('user')
// 控制器级别使用:这个控制器所有接口都用统一响应格式
@UseInterceptors(TransformResponseInterceptor)
export class UserController {
@Get('/profile')
getProfile() {
// 接口返回原始数据:{id: 1, name: "张三"}
return { id: 1, name: "张三" };
}
}
效果:响应格式做出用统一的修改
注册方式
局部注册:直接使用@UseInterceptors
装饰器加在接口方法上面
控制器级别注册:给整个控制器的所有接口使用加在控制器类上
同样全局注册具有局限性
整体响应流程
当请求同时遇到Guard,Pipe,Interceptor,Filter时候,执行顺序很关键,大概流程是
全局interceptor(请求阶段)→ 全局Guard → 控制器级Guard → 接口级Guard → 全局Pipe → 控制器pipe → 接口Pipe → 控制器interceptor(请求阶段) → 接口级别interceptor → 接口方法执行 → 接口 interceptor → 控制器 interceptor → 全局的interceptor(响应阶段) → 如果报错 Filter执行