Nest 基础知识

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.tsT应用程序的根模块

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()装饰器

      typescript 复制代码
      import {
        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,每页默认10tiao

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 参数的类型(比如 StringNumber 或自定义 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执行

相关推荐
koooo~2 小时前
Vue3中的依赖注入
前端·javascript·vue.js
沢田纲吉2 小时前
《LLVM IR 学习手记(三):赋值表达式与错误处理的实现与解析》
前端·编程语言·llvm
sophie旭2 小时前
一道面试题,开始性能优化之旅(3)-- DNS查询+TCP(一)
前端·面试·性能优化
IT_陈寒3 小时前
JavaScript性能优化:这7个V8引擎技巧让我的应用速度提升了50%
前端·人工智能·后端
学渣y3 小时前
nvm下载node版本,npm -v查看版本报错
前端·npm·node.js
excel3 小时前
首屏加载优化总结
前端
敲代码的嘎仔3 小时前
JavaWeb零基础学习Day1——HTML&CSS
java·开发语言·前端·css·学习·html·学习方法
Tachyon.xue5 小时前
Vue 3 项目集成 Element Plus + Tailwind CSS 详细教程
前端·css·vue.js
FuckPatience6 小时前
Vue 中‘$‘符号含义
前端·javascript·vue.js