NestJS 核心基础
$ npm i -g @nestjs/cli
$ nest new project-name
热重载启动:npm run start:dev
创建 controller:nest g controller xxx
创建 service:nest g service xxx(不用写目录结构,如果是 user 目录里面,我们在外面也直接写 user 即可)
创建 module:nest g module xxx
1.Controller 控制器
1.创建控制器目录
nest g controller cats(和 angular 很像)
注意:目录结构会决定请求路由!
2.controller 写法
import { Controller, Get } from "@nestjs/common";
@Controller("cats")
export class CatsController {
@Get()
findAll(): string {
//注意:路由与处理函数命名无关
return "This action returns all cats";
/**
* 使用这个内置方法,当请求处理程序返回一个 JavaScript 对象或数组时,它将自动序列化为 JSON。
* 但是,当它返回一个 JavaScript 基本类型(例如string、number、boolean)时,Nest 将只发送值,而不尝试序列化它。
* 这使响应处理变得简单:只需要返回值,其余的由 Nest 负责。
*/
}
}
3.原生 http 对象
1.response 对象
在函数签名处通过 @Res()
注入类库特定的响应对象(例如, Express
)。使用此方法,你就能使用由该响应对象暴露的原生响应处理函数。例如,使用 Express
,您可以使用 response.status(200).send()
构建响应
示例代码:
import { Controller, Get, Post, Res, HttpStatus } from "@nestjs/common";
import { Response } from "express";
@Controller("cats")
export class CatsController {
@Post()
create(@Res() res: Response) {
res.status(HttpStatus.CREATED).send();
}
@Get()
findAll(@Res() res: Response) {
res.status(HttpStatus.OK).json([]);
}
}
尽管此方法有效,并且实际上通过提供对响应对象的完全控制(标头操作,特定于库的功能等)在某些方面提供了更大的灵活性,但应谨慎使用此种方法。通常来说,这种方式非常不清晰,并且有一些缺点。 主要的缺点是你的代码变得依赖于平台(因为不同的底层库在响应对象(Response)上可能具有不同的 API),并且更加难以测试(您必须模拟响应对象等)。
而且,在上面的示例中,你失去与依赖于 Nest 标准响应处理的 Nest 功能(例如,拦截器(Interceptors) 和
@HttpCode()
/@Header()
装饰器)的兼容性。要解决此问题,可以将passthrough
选项设置为true
,如下所示:typescript@Get() findAll(@Res({ passthrough: true }) res: Response) { res.status(HttpStatus.OK); return []; }
现在,你就能与底层框架原生的响应对象(Response)进行交互(例如,根据特定条件设置 Cookie 或 HTTP 头),并将剩余的部分留给 Nest 处理。
2.request 对象
处理程序有时需要访问客户端的请求细节。Nest 提供了对底层平台(默认为 Express
)的请求对象(request
)的访问方式。我们可以在处理函数的签名中使用 @Req()
装饰器,指示 Nest 将请求对象注入处理程序。
但是注意:Request
对象代表 HTTP
请求,并具有查询字符串,请求参数参数,HTTP 标头(HTTP header) 和 正文(HTTP body)的属性(在这里阅读更多)。在多数情况下,不必手动获取它们。
比如开箱即用的 @Body()
或 @Query()
4.HTTP 装饰器
Nest 为所有标准的 HTTP 方法提供了相应的装饰器:@Put()
、@Delete()
、@Patch()
、@Options()
、以及 @Head()
。此外,@All()
则用于定义一个用于处理所有 HTTP 请求方法的处理程序。
5.路由通配符
路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。
@Get('ab*cd')
findAll() {
return 'This route uses a wildcard';
}
路由路径 'ab*cd'
将匹配 abcd
、ab_cd
、abecd
等。字符 ?
、+
、 *
以及 ()
是它们的正则表达式对应项的子集。连字符(-
) 和点(.
)按字符串路径逐字解析。
6.状态码
如上所述,默认情况下,响应的状态码总是默认为 200,除了 POST 请求(默认响应状态码为 201)。
我们可以通过在处理函数外添加 @HttpCode(...)
装饰器来轻松更改此行为。
//HttpCode 需要从 @nestjs/common 包导入。
@Post()
@HttpCode(204)
create() {
return 'This action adds a new cat';
}
通常,状态码不是固定的,而是取决于各种因素。在这种情况下,您可以使用 response
(通过 @Res()
注入 )对象(或者在出现错误时,抛出异常)。
7.Header
要指定自定义响应头,可以使用 @header()
装饰器
//Header 需要从 @nestjs/common 包导入。
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
8.重定向
要将响应重定向到特定的 URL
,可以使用 @Redirect()
装饰器或特定于库的响应对象(并直接调用 res.redirect()
)。
@Redirect()
装饰器有两个可选参数,url
和 statusCode
。 如果省略,则 statusCode
默认为 302
。
@Get()
@Redirect('https://nestjs.com', 301)
9.路由参数 get
当您需要接受动态数据(dynamic data)作为请求的一部分时(例如,使用GET /cats/1
)
以这种方式声明的路由参数可以使用 @Param()
装饰器访问,该装饰器应添加到函数签名中。
//Param 需要从 @nestjs/common 包导入。
@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
您还可以将特定的参数标记传递给装饰器,然后在方法主体中按参数名称直接引用路由参数。
@Get(':id')
findOne(@Param('id') id): string {
return `This action returns a #${id} cat`;
}
query 参数 get
@Get()
findAll(@Query() query: ListAllEntities) {
return `This action returns all cats (limit: ${query.limit} items)`;
}
get 查询中路由参数和查询参数(query 参数)的区别:
在 Web 开发中,路由参数和查询参数是两种常见的 URL 参数传递方式,它们有一些区别:
路由参数(Route Parameters):
- 路由参数是将数据作为 URL 的一部分进行传递的一种方式。
- 在 URL 中,路由参数通常出现在路径的一部分,并用于标识资源或指定特定的操作。
- 例如,在 RESTful API 中,可以使用路由参数来指定资源的 ID 或标识要执行的特定操作。
- 路由参数可以通过 URL 的路径部分进行访问,但不会在查询字符串中显示。
- 在 Express.js 等 Web 框架中,可以通过定义路由路径中的参数来访问路由参数。
示例:
bashGET /users/:userId GET /posts/:postId/comments/:commentId
查询参数(Query Parameters):
- 查询参数是将数据作为键值对形式添加到 URL 的查询字符串部分的一种方式。
- 在 URL 中,查询参数通常跟在问号(?)之后,并使用等号(=)将键和值连接起来,多个参数之间使用和号(&)分隔。
- 查询参数用于向服务器传递附加的数据,例如过滤、排序或限制结果等。
- 查询参数可以在 URL 中以键值对的形式轻松识别和解析。
- 在 Express.js 等 Web 框架中,可以通过解析请求对象中的查询参数来访问查询参数。
示例:
bashGET /users?role=admin&status=active GET /search?q=query&page=1&limit=10
总的来说,路由参数用于标识资源或指定操作,而查询参数用于向服务器传递附加的数据。它们在 URL 中的位置和使用方式略有不同,因此在设计 API 时需要根据需求选择合适的参数传递方式。
10.请求负载 post
此前我们列举的的 POST
路由处理程序样例中,处理程序没有接受任何客户端参数。我们在这里通过添加 @Body()
参数来解决这个问题。
首先(如果您使用 TypeScript),我们需要确定 DTO
(数据传输对象)模式。DTO
是一个对象,它定义了如何通过网络发送数据。
现在,我们来创建 CreateCatDto
类:
/*
create-cat.dto.ts
*/
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
然后,我们可以在 CatsController
中使用新创建的DTO
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
11.异步性
每个异步函数都必须返回一个 Promise
。这意味着您可以返回延迟值,而 Nest 将自行解析它。
@Get()
async findAll(): Promise<any[]> {
return []; //async包裹的函数会自动返回Promise
}
这是完全有效的。此外,通过返回 RxJS observable 流,Nest 路由处理程序将更加强大。 Nest 将自动订阅下面的源并获取最后发出的值(在流完成后)。
@Get()
findAll(): Observable<any[]> {
return of([]);
}
上述的两种方法都是可行的,你可以选择你喜欢的方式。
12.完整示例
import {
Controller,
Get,
Query,
Post,
Body,
Put,
Param,
Delete,
} from "@nestjs/common";
import { CreateCatDto, UpdateCatDto, ListAllEntities } from "./dto";
@Controller("cats")
export class CatsController {
@Post()
create(@Body() createCatDto: CreateCatDto) {
return "This action adds a new cat";
}
@Get()
findAll(@Query() query: ListAllEntities) {
return `This action returns all cats (limit: ${query.limit} items)`;
}
@Get(":id")
findOne(@Param("id") id: string) {
return `This action returns a #${id} cat`;
}
@Put(":id")
update(@Param("id") id: string, @Body() updateCatDto: UpdateCatDto) {
return `This action updates a #${id} cat`;
}
@Delete(":id")
remove(@Param("id") id: string) {
return `This action removes a #${id} cat`;
}
}
Nest CLI
提供了一个能够自动生成所有这些模板代码的生成器,它帮助我们规避手动建立这些文件,并使开发体验变得更加简单。在这里阅读关于该功能的更多信息。
13.注册控制器到模块
控制器已经准备就绪,可以使用,但是 Nest 依然不知道 CatsController
是否存在,所以它不会创建这个类的一个实例。
控制器总是属于模块,这就是为什么我们在 @Module()
装饰器中包含 controllers
数组的原因。
import { Module } from "@nestjs/common";
import { CatsController } from "./cats/cats.controller";
@Module({
controllers: [CatsController],
})
export class AppModule {}
现在,Nest 可以轻松反射(reflect)出哪些控制器(controller)必须被安装。
2.Service 服务
1.Providers 提供者
Providers 是 Nest
的一个基本概念。许多基本的 Nest
类都可能被视为 provider - service
, repository
, factory
, helper
等等。 他们都可以通过 constructor
注入依赖关系。 这意味着对象可以彼此创建各种关系,并且“连接”对象实例的功能在很大程度上可以委托给 Nest
运行时系统。 Provider 只是一个用 @Injectable()
装饰器注释的类。
类似于 springboot 里面的依赖注入的感觉!
2.Service 介绍
让我们从创建一个简单的 CatsService
开始。该服务将负责数据存储和检索,其由 CatsController
使用,因此把它定义为 provider
,是一个很好的选择。因此,我们用 @Injectable()
来装饰这个类 。
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
//interfaces/cat.interface.ts
export interface Cat {
name: string;
age: number;
breed: string;
}
创建服务类的命令:
nest g service cats
现在我们有一个服务类来检索 cat
,让我们在 CatsController
里使用它 :
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
//注入依赖关系
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto); //操作cats,新增
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll(); //操作cats,查询
}
}
3.Service 特性
1.依赖注入
Nest 是建立在强大的设计模式,通常称为依赖注入。我们建议在官方的 Angular 文档中阅读有关此概念的精彩文章。
在 Nest
中,借助 TypeScript 功能,管理依赖项非常容易,因为它们仅按类型进行解析。在下面的示例中,Nest
将 catsService
通过创建并返回一个实例来解析 CatsService
(或者,在单例的正常情况下,如果现有实例已在其他地方请求,则返回现有实例)。解析此依赖关系并将其传递给控制器的构造函数(或分配给指定的属性):
constructor(private readonly catsService: CatsService) {}
2.作用域
Provider 通常具有与应用程序生命周期同步的生命周期(“作用域”)。在启动应用程序时,必须解析每个依赖项,因此必须实例化每个提供程序。同样,当应用程序关闭时,每个 provider 都将被销毁。但是,有一些方法可以改变 provider 生命周期的请求范围。
3.自定义提供者
Nest
有一个内置的控制反转("IoC"
)容器,可以解决 providers 之间的关系。此功能是上述依赖注入功能的基础,但要比上面描述的要强大得多。
@Injectable()
装饰器只是冰山一角, 并不是定义 providers 的唯一方法。相反,您可以使用普通值、类、异步或同步工厂。看看这里找到更多的例子。
4.可选提供者
有时,您可能需要解决一些依赖项。例如,您的类可能依赖于一个配置对象,但如果没有传递,则应使用默认值。在这种情况下,关联变为可选的, provider
不会因为缺少配置导致错误。
要指示 provider 是可选的,请在 constructor
的参数中使用 @Optional()
装饰器。
比如这里 HttpService 需要注入相应的自定义 provider:HTTP_OPTIONS
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
constructor(
@Optional() @Inject('HTTP_OPTIONS') private readonly httpClient: T
) {}
}
请注意,在上面的示例中,我们使用自定义 provider
,这是我们包含 HTTP_OPTIONS
自定义标记的原因。
5.基于属性的注入(非主流)
我们目前使用的技术称为基于构造函数的注入,即通过构造函数方法注入 providers。在某些非常特殊的情况下,基于属性的注入可能会有用。 ——> 和 springboot 同理
例如,如果顶级类依赖于一个或多个 providers,那么通过从构造函数中调用子类中的 super()
来传递它们就会非常烦人了。因此,为了避免出现这种情况,可以在属性上使用 @Inject()
装饰器。
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
注意:如果您的类没有扩展其他提供者,你应该总是使用基于构造函数的注入。
6.注册提供者
现在我们已经定义了提供者(CatsService
),并且已经有了该服务的使用者(CatsController
),我们需要在 Nest
中注册该服务,以便它可以执行注入。 为此,我们可以编辑模块文件(app.module.ts
),然后将服务添加到@Module()
装饰器的 providers
数组中。
import { Module } from "@nestjs/common";
import { CatsController } from "./cats/cats.controller";
import { CatsService } from "./cats/cats.service";
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
得益于此,Nest
现在将能够解决 CatsController
类的依赖关系(需要的服务)。这就是我们目前的目录结构:
src
├── cats
│ ├──dto
│ │ └──create-cat.dto.ts
│ ├──interfaces
│ │ └──cat.interface.ts
│ ├──cats.service.ts
│ └──cats.controller.ts
├──app.module.ts
└──main.ts
7.手动实例化
到目前为止,我们已经讨论了 Nest 如何自动处理解决依赖关系的大多数细节。在某些情况下,您可能需要跳出内置的依赖注入系统,并手动检索或实例化提供程序。我们在下面简要讨论两个这样的主题。
要获取现有实例或动态实例化提供程序,可以使用 Module reference。
要在 bootstrap()
函数内使用提供程序(例如,对于不带控制器的独立应用程序,或在引导过程中使用配置服务),请参见独立应用程序。
3.Module 模块
1.基本介绍
模块是具有 @Module()
装饰器的类。 @Module()
装饰器提供了元数据,Nest 用它来组织应用程序结构。

每个 Nest 应用程序至少有一个模块,即根模块。在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能。
@module()
装饰器接受一个描述模块属性的对象:
providers | 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享 |
---|---|
controllers | 必须创建的一组控制器 |
imports | 导入模块的列表,这些模块导出了此模块中所需提供者 |
exports | 由本模块提供并应在其他模块中可用的提供者的子集。 |
默认情况下,该模块封装提供程序。这意味着无法注入既不是当前模块的直接组成部分(模块的本身的程序),也不是从导入的模块导出的提供程序(导入进来的模块的暴露的程序)。
将从模块导出的提供程序被视为模块的公共接口或 API。
2.功能模块
CatsController
和 CatsService
属于同一个应用程序域。 应该考虑将它们移动到一个功能模块下,即 CatsModule
。
import { Module } from "@nestjs/common";
import { CatsController } from "./cats.controller";
import { CatsService } from "./cats.service";
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
创建模块:
要使用 CLI 创建模块,只需执行 $ nest g module cats
命令。
我们需要做的最后一件事是将这个模块导入根模块 (ApplicationModule)
。
app.module.ts
import { Module } from "@nestjs/common";
import { CatsModule } from "./cats/cats.module";
@Module({
imports: [CatsModule],
})
export class ApplicationModule {}
注:模块基本目录结构
现在 Nest
知道除了 ApplicationModule
之外,注册 CatsModule
也是非常重要的。 这就是我们现在的目录结构:
src
├──cats
│ ├──dto
│ │ └──create-cat.dto.ts
│ ├──interfaces
│ │ └──cat.interface.ts
│ ├─cats.service.ts
│ ├─cats.controller.ts
│ └──cats.module.ts
├──app.module.ts
└──main.ts
3.共享模块

在 Nest 中,默认情况下,模块是单例,因此您可以轻松地在多个模块之间共享同一个提供者实例(只需要每个模块都引入那个单例的共享模块即可)。
实际上,每个模块都是一个共享模块。一旦创建就能被任意模块重复使用。假设我们将在几个模块之间共享 CatsService
实例。 我们需要把 CatsService
放到 exports
数组中,如下所示:
import { Module } from "@nestjs/common";
import { CatsController } from "./cats.controller";
import { CatsService } from "./cats.service";
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
现在,每个导入 CatsModule
的模块都可以访问 CatsService
,并且它们将共享相同的 CatsService
实例。
4.模块导出
模块可以导出他们的内部提供者。 而且,他们可以再导出自己导入的模块。
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
这样别的模块导入了 CoreModule 就相当于同时导入了 CoreModule 和 CommonModule
5.依赖注入
提供者也可以注入到模块(类)中(例如,用于配置目的):
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private readonly catsService: CatsService) {}
}
6.全局模块
如果你不得不在任何地方导入相同的模块,那可能很烦人。在 Angular 中,提供者是在全局范围内注册的。一旦定义,他们到处可用。
但是 Nest 将提供者封装在模块范围内。您无法在其他地方使用模块的提供者而不导入他们。但是有时候,你可能想提供一组随时可用的东西(service) - 例如:helper,数据库连接等等。这就是为什么你能够使模块成为全局模块。
import { Module, Global } from "@nestjs/common";
import { CatsController } from "./cats.controller";
import { CatsService } from "./cats.service";
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
@Global
装饰器使模块成为全局作用域。 全局模块应该只注册一次,最好由根或核心模块注册。 在上面的例子中,CatsService
组件将无处不在,而想要使用 CatsService
的模块则不需要在 imports
数组中导入 CatsModule
。
使一切全局化并不是一个好的解决方案。 全局模块可用于减少必要模板文件的数量。
imports
数组仍然是使模块 API 透明的最佳方式。
7.动态模块
Nest
模块系统包括一个称为动态模块的强大功能。此功能使您可以轻松创建可自定义的模块,这些模块可以动态注册和配置提供程序。
理解:模块可以根据传入的参数动态创建,生成不同类别的 providers 等内容!
以下是一个动态模块定义的示例 DatabaseModule
:
import { Module, DynamicModule } from "@nestjs/common";
import { createDatabaseProviders } from "./database.providers";
import { Connection } from "./connection.provider";
@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
forRoot()
可以同步或异步(Promise
)返回动态模块。
此模块 Connection
默认情况下(在 @Module()
装饰器元数据中)定义提供程序,但此外-根据传递给方法的 entities
和 options
对象 forRoot()
-公开提供程序的集合,例如存储库。
请注意,动态模块返回的属性扩展(而不是覆盖)@Module()
装饰器中定义的基本模块元数据(这样 Module 本身也可以定义一些静态的内容)。这就是从模块导出静态声明的 Connection
提供程序和动态生成的存储库提供程序的方式。
如果要在全局范围内注册动态模块,请将 global
属性设置为 true
。
return {
global: true,
module: DatabaseModule,
providers: providers,
exports: providers,
};
上面的动态模块 DatabaseModule
可以被导入,并且被配置以下列方式:
import { Module } from "@nestjs/common";
import { DatabaseModule } from "./database/database.module";
import { User } from "./users/entities/user.entity";
@Module({
imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}
如果要重新导出动态模块,则可以 forRoot()
在导出数组中省略方法调用:
import { Module } from "@nestjs/common";
import { DatabaseModule } from "./database/database.module";
import { User } from "./users/entities/user.entity";
@Module({
imports: [DatabaseModule.forRoot([User])],
exports: [DatabaseModule],
})
export class AppModule {}
4.Middleware 中间件
注意:中间件也是一种提供者 providers!但是他和 controller 是一个级别的,比 service 要高一个层级,因为它可以依赖注入 service,但是 service 不能互相依赖注入,而是中间件是 class 而 service 是 function!
中间件是在路由处理程序 之前 调用的函数。 中间件函数可以访问请求和响应对象,以及应用程序请求响应周期中的 next()
中间件函数。next()
中间件函数通常由名为 next
的变量表示。
中间件也就是位于客户端和 controller 路由响应之间的部分,就是中间件处理的部分!(可以影响请求对象和响应对象)
Nest 中间件实际上等价于 express 中间件。 下面是 Express 官方文档中所述的中间件功能:
中间件函数可以执行以下任务:
- 执行任何代码。
- 对请求和响应对象进行更改。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件函数。
- 如果当前的中间件函数没有结束请求-响应周期, 它必须调用
next()
将控制传递给下一个中间件函数。否则, 请求将被挂起。
logger.middleware.ts
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("Request...");
next();
}
}
1.依赖注入
Nest
中间件完全支持依赖注入。 就像提供者和控制器一样,它们能够注入属于同一模块的依赖项(通过 constructor
)。
2.中间件消费者
apply 和 forRoutes 方法都是 MiddlewareConsumer 上面的方法!这里进行系统的介绍!
MiddlewareConsumer
是一个帮助类。它提供了几种内置方法来管理中间件。他们都可以被简单地链接起来。forRoutes()
可接受一个字符串、多个字符串、对象、一个控制器类甚至多个控制器类。在大多数情况下,您可能只会传递一个由逗号分隔的控制器列表。以下是单个控制器的示例:
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common";
import { LoggerMiddleware } from "./common/middleware/logger.middleware";
import { CatsModule } from "./cats/cats.module";
import { CatsController } from "./cats/cats.controller.ts";
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes(CatsController); //相当于把中间件作用于整个控制器
}
}
注意:该
apply()
方法可以使用单个中间件,也可以使用多个参数来指定多个多个中间件jsxconsumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
有时我们想从应用中间件中排除某些路由。我们可以使用该 exclude()
方法轻松排除某些路由。此方法可以采用一个字符串,多个字符串或一个 RouteInfo
对象来标识要排除的路由,如下所示:
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: "cats", method: RequestMethod.GET },
{ path: "cats", method: RequestMethod.POST },
"cats/(.*)"
)
.forRoutes(CatsController);
该
exclude()
方法使用path-to-regexp
包支持通配符参数。
3.应用中间件
注意:和 controller 的注册方式不一样!
中间件不能在 @Module()
装饰器中列出。我们必须使用模块类的 configure()
方法来设置它们。包含中间件的模块必须实现 NestModule
接口。我们将 LoggerMiddleware
设置在 ApplicationModule
层上。
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 {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes("cats");
}
}
上面我们用中间件消费者 MiddlewareConsumer 的 apply 方法为 App 模块设置了中间件LoggerMiddleware
。
同时将特定的请求路径传递给 forRoutes()
方法,从而进一步将中间件限制为服务于特定的 CatsController 里面特定的 cats 请求路径!
我们还可以在配置中间件时将包含路由路径的对象和请求方法传递给 forRoutes()
方法,从而进一步将中间件限制为特定的请求方式。下面为示例:
注意:我们导入了 RequestMethod
来引用所需的请求方法类型
import {
Module,
NestModule,
RequestMethod,
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 {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: "cats", method: RequestMethod.GET });
}
}
可以使用
async/await
来实现configure()
方法的异步化(例如,可以在configure()
方法体中等待异步操作的完成)。
4.路由通配符
路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。
forRoutes({ path: "ab*cd", method: RequestMethod.ALL });
以上路由地址将匹配 abcd
、 ab_cd
、 abecd
等。字符 ?
、 +
、 *
以及 ()
是它们的正则表达式对应项的子集。连字符 (-
) 和点 (.
) 按字符串路径解析。
5.函数式中间件
我们使用的 LoggerMiddleware
类非常简单。它没有成员,没有额外的方法,没有依赖关系。为什么我们不能只使用一个简单的函数?这是一个很好的问题,因为事实上 - 我们可以做到。这种类型的中间件称为函数式中间件。让我们把 logger
转换成函数。
logger.middleware.ts
export function logger(req, res, next) {
console.log(`Request...`);
next();
}
现在在 AppModule
中使用它。
consumer.apply(logger).forRoutes(CatsController);
当您的中间件没有任何依赖关系时,我们可以考虑使用函数式中间件。
注意:这并没有改变中间件的级别,它依然是比 service 高一级的!
6.全局中间件
如果我们想一次性将中间件绑定到每个注册路由,我们可以使用由INestApplication
实例提供的 use()
方法:
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
5.Pipe 管道
1.基本介绍
注意:管道也是一种提供者 providers,主要作用于 controller
管道是具有 @Injectable()
装饰器的类。管道应实现 PipeTransform
接口。
管道有两个典型的应用场景:
- 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
- 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常
在这两种情况下, 管道 参数(arguments)
会由 控制器(controllers)的路由处理程序 进行处理。Nest 会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转换或是验证处理,然后用转换好或是验证好的参数调用原方法。
Nest 自带很多开箱即用的内置管道。你还可以构建自定义管道。
2.内置管道
Nest
自带九个开箱即用的管道,即
验证管道:
ValidationPipe
转换管道:
ParseIntPipe
ParseFloatPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
ParseEnumPipe
DefaultValuePipe
ParseFilePipe
我们先来快速看看如何使用ParseIntPipe
。这是一个转换的应用场景,管道确保传给路由处理程序的参数是一个整数(若不是整数,就进行转换;若转换失败,则抛出异常)。
我们传递了一个类(ParseIntPipe
),而不是一个实例,将实例化留给框架去处理,做到了依赖注入。
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
这确保了我们在 findOne()
方法中接收的参数是一个数字(与 this.catsService.findOne()
方法的诉求一致),或者在路由处理程序被调用之前抛出异常。
举个例子,假设路由是这样子的
GET localhost:3000/abc
Nest 将会抛出这样的异常:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}
这个异常阻止了 findOne()
方法的执行。
使用 ParseUUIDPipe
解析字符串并验证是否为 UUID 的例子
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}
对于管道和守卫,我们也可以选择传递一个实例。如果我们想通过传递选项来自定义内置管道的行为,传递实例很有用:
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}
3.自定义管道
让我们从头开始构建内置管道 ValidationPipe 和 ParseIntPipe 的简单自定义版本,以了解如何构建自定义管道。
validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from "@nestjs/common";
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
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 {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException("Validation failed");
}
return val;
}
}
PipeTransform<T, R>
是每个管道必须要实现的泛型接口。泛型T
表明输入的value
的类型,R
表明transfrom()
方法的返回类型
为实现 PipeTransfrom
,每个管道必须声明 transfrom()
方法。该方法有两个参数:
value
metadata
value
参数是当前处理的方法参数(在被路由处理程序方法接收之前),metadata
是当前处理的方法参数的元数据。元数据对象具有以下属性:
export interface ArgumentMetadata {
type: "body" | "query" | "param" | "custom";
metatype?: Type<unknown>;
data?: string;
}
这些属性描述了当前处理的参数。
参数 | 描述 |
---|---|
type | 告诉我们参数是一个 body @Body() ,query @Query() ,param @Param() 还是自定义参数 在这里阅读更多。 |
metatype | 参数的元类型,例如 String 。 如果在函数签名中省略类型声明,或者使用原生 JavaScript(ts 才需要),则为 undefined 。 |
data | 传递给装饰器的字符串,例如 @Body('string') 。如果您将括号留空,则为 undefined 。 |
TypeScript 中的 interface 在转译期间会消失。因此,如果方法参数的类型被声明为接口(interface)而不是类(class),则
metatype
将是Object
。——> 因为 undefined 的类型是 Object
4.对象结构验证
让我们把验证管道变得更有用一点。仔细看看 CatsController
的 create()
方法,我们希望在该方法被调用之前,请求主体(post body)得到验证。
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
有几种方法可以实现。一种常见的方式是使用基于结构的验证。我们来尝试一下。
1.Joi 库(简单)
Joi 库允许使用可读的 API 以直接的方式创建 schema,让我们构建一个基于 Joi schema 的验证管道。
首先安装依赖:
$ npm install --save joi
$ npm install --save-dev @types/joi
在下面的代码中,我们先创建一个简单的 class,在构造函数中传递 schema 参数。然后我们使用 schema.validate()
方法验证参数是否符合提供的 schema。
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ObjectSchema } from '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;
}
}
绑定/应用验证管道
在之前,我们已经了解如何绑定转换管道(像 ParseIntPipe
和其他 Parse*
管道)。
绑定验证管道也十分直截了当。
在这种情况下,我们希望在方法调用级别绑定管道。在当前示例中,我们需要执行以下操作使用 JoiValidationPipe
:
- 创建一个
JoiValidationPipe
实例 - 传递上下文特定的 Joi schema 给构造函数
- 绑定到方法
//从 @nestjs/common 包导入 @UsePipes() 装饰器
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema)) //createCatSchema是什么样子的?
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
export class CreateCatDto {
name: string; //后面的string不是值,而是类型声明
age: number;
breed: string;
}
2.类验证器(强大)(难)
本节中的技术需要
TypeScript
,如果您的应用是使用原始JavaScript
编写的,则这些技术不可用。
让我们看一下验证的另外一种实现方式。
Nest 与 class-validator 配合得很好。这个优秀的库允许您使用基于装饰器的验证。装饰器的功能非常强大,尤其是与 Nest 的 Pipe 功能相结合使用时,因为我们可以通过访问 metatype
信息做很多事情,在开始之前需要安装一些依赖。
$ npm i --save class-validator class-transformer
安装完成后,我们就可以向 CreateCatDto
类添加一些装饰器。在这里,我们看到了这种技术实现的一个显著优势:CreateCatDto
类仍然是我们的 Post body 对象的单一可靠来源(而不是必须创建一个单独的验证类)。
create-cat.dto.ts
import { IsString, IsInt } from "class-validator";
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
现在我们来创建一个 ValidationPipe
类。
validate.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
太复杂!
主要是可以自定义一个范围选项的类型判断,更加强大和定制化!
5.全局管道
由于 ValidationPipe
被创建为尽可能通用,所以我们将把它设置为一个全局作用域的管道,用于整个应用程序中的每个路由处理器。
main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
在 混合应用中
useGlobalPipes()
方法不会为网关和微服务设置管道, 对于标准(非混合) 微服务应用使用useGlobalPipes()
全局设置管道。
但是,从任何模块外部注册的全局管道(即使用了 useGlobalPipes()
, 如上例所示)无法注入依赖,因为它们不属于任何模块。为了解决这个问题,可以使用以下构造直接为任何模块设置管道:
import { Module } from "@nestjs/common";
import { APP_PIPE } from "@nestjs/core";
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
export class AppModule {}
6.转换管道的应用场景
验证不是管道唯一的用处。在本章的开始部分,我已经提到管道也可以将输入数据转换为所需的输出。这是可以的,因为从 transform
函数返回的值完全覆盖了参数先前的值。
在什么时候有用?有时从客户端传来的数据需要经过一些修改(例如字符串转化为整数),然后处理函数才能正确的处理。还有种情况,有些数据的必填字段缺失,那么可以使用默认值。
转换管道被插入在客户端请求和请求处理程序之间用来处理客户端请求。(和中间件的位置一样)
另一个有用的例子是按 ID 从数据库中选择一个现有的用户实体。
这种就是以小博大的感觉,用一个小的 id 换取一个大的 user 实体,封装成公共管道,是有很大的作用的!可以减少代码!
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity;
}
//扩展成如下代码:
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeFindOneUser<number, UserEntity> {
transform(value: number, metadata: ArgumentMetadata): UserEntity {
//result = 数据库查询语句
if (!result) {
throw new BadRequestException('Validation failed');
}
return result;
}
}
这个管道接收 id 参数并返回 UserEntity 数据, 这样做就可以抽象出一个根据 id 得到 UserEntity 的公共管道, 你的程序变得更符合声明式(Declarative 更好的代码语义和封装方式), 更 DRY (Don’t repeat yourself 减少重复代码) 编程规范.
7.为管道提供默认值
**Parse*
管道期望参数值是被定义的。当接收到 null
或者 undefined
值时,它们会抛出异常。**为了允许端点处理丢失的查询字符串参数值,我们必须在 Parse*
管道对这些值进行操作之前注入默认值。DefaultValuePipe
提供了这种能力。只需在相关 Parse*
管道之前的 @Query()
装饰器中实例化 DefaultValuePipe
,如下所示:
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}
6.Intercept 拦截器
1.介绍
拦截器是使用 @Injectable()
装饰器注解的类。拦截器应该实现 NestInterceptor
接口
拦截器也是一种提供者 providers!和中间件、管道所处的位置也基本一样!
拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 扩展基本函数行为
- 根据所选条件完全重写函数 (例如, 缓存目的)
就是各种玩弄操作函数!
2.参数
每个拦截器都有 intercept()
方法,它接收 2 个参数。
1.ExecutionContext
第一个是 ExecutionContext
实例(与守卫完全相同的对象)。 ExecutionContext
继承自 ArgumentsHost
。 ArgumentsHost
是传递给原始处理程序的参数的一个包装,它根据应用程序的类型包含不同的参数数组。你可以在这里读更多关于它的内容(在异常过滤器章节中)。
通过扩展 ArgumentsHost
,ExecutionContext
还添加了几个新的帮助程序方法,这些方法提供有关当前执行过程的更多详细信息。这些详细信息有助于构建可以在广泛的控制器,方法和执行上下文中使用的更通用的拦截器。ExecutionContext
在此处了解更多信息。
2.CallHandler
第二个参数是 CallHandler
。如果不手动调用 handle()
方法,则主处理程序根本不会进行求值。这是什么意思?基本上,CallHandler
是一个包装执行流的对象,因此推迟了最终的处理程序执行。
比方说,有人提出了 POST /cats
请求。此请求指向在 CatsController
中定义的 create()
处理程序。如果在此过程中未调用拦截器的 handle()
方法,则 create()
方法不会被计算。只有 handle()
被调用(并且已返回值),最终方法才会被触发。为什么?因为 Nest 订阅了返回的流,并使用此流生成的值来为最终用户创建单个响应或多个响应。而且,handle()
返回一个 Observable
,这意味着它为我们提供了一组非常强大的运算符,可以帮助我们进行例如响应操作。
3.截取切面
使用拦截器在函数执行之前或之后添加额外的逻辑。
当我们要记录与应用程序的交互时,它很有用,例如 存储用户调用,异步调度事件或计算时间戳。作为一个例子,我们来创建一个简单的例子 LoggingInterceptor
。
logging.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log("Before...");
const now = Date.now();
return next
.handle()
.pipe(tap(() => console.log(`After... ${Date.now() - now}ms`)));
}
}
NestInterceptor<T,R>
是一个通用接口,其中T
表示已处理的Observable<T>
的类型(在流后面),而R
表示包含在返回的Observable<R>
中的值的返回类型。
拦截器的作用与控制器,提供程序,守卫等相同,这意味着它们可以通过构造函数注入依赖项。
由于 handle()
返回一个 RxJS Observable
,我们有很多种操作符可以用来操作流。在上面的例子中,我们使用了 tap()
运算符,该运算符在可观察序列的正常或异常终止时调用函数。
绑定拦截器
为了设置拦截器, 我们使用从 @nestjs/common
包导入的 @UseInterceptors()
装饰器。与守卫一样, 拦截器可以是控制器范围内的, 方法范围内的或者全局范围内的
cats.controller.ts
// @UseInterceptors() 装饰器从 @nestjs/common 导入。
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
由此,CatsController
中定义的每个路由处理程序都将使用 LoggingInterceptor
。当有人调用 GET /cats
端点时,您将在控制台窗口中看到以下输出:
Before...
After... 1ms
请注意,我们传递的是
LoggingInterceptor
类型而不是实例,让框架承担实例化责任并启用依赖注入。另一种可用的方法是传递立即创建的实例:和中间件同理
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
4.全局拦截器
如上所述, 上面的构造将拦截器附加到此控制器声明的每个处理程序。如果我们决定只限制其中一个, 我们只需在方法级别设置拦截器。为了绑定全局拦截器, 我们使用 Nest 应用程序实例的 useGlobalInterceptors()
方法:
const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());
全局拦截器用于整个应用程序、每个控制器和每个路由处理程序。在依赖注入方面, 从任何模块外部注册的全局拦截器 (如上面的示例中所示) 无法插入依赖项, 因为它们不属于任何模块。为了解决此问题, 您可以使用以下构造直接从任何模块设置一个拦截器: 和全局管道同理
import { Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
5.响应映射
我们已经知道, handle()
返回一个 Observable
。此流包含从路由处理程序返回的值, 因此我们可以使用 map()
运算符轻松地对其进行改变。
简单来说,就是把函数的返回值(比如请求响应)进行相应的修改映射!
注意:响应映射功能不适用于特定于库的响应策略(禁止直接使用
@Res()
对象)。
让我们创建一个 TransformInterceptor, 它将打包响应并将其分配给 data 属性。
transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<Response<T>> {
return next.handle().pipe(map((data) => ({ data }))); //pipe是Observable对象的方法!管道,可以进行映射转换!
}
}
Nest
拦截器就像使用异步intercept()
方法的魅力一样, 意思是, 如果需要,您可以毫不费力地将方法切换为异步。
拦截器在创建用于整个应用程序的可重用解决方案时具有巨大的潜力。
例如,我们假设我们需要将每个发生的 null
值转换为空字符串 ''
。我们可以使用一行代码并将拦截器绑定为全局代码。由于这一点,它会被每个注册的处理程序自动重用。
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map((value) => (value === null ? "" : value)));
}
}
让所有返回 null 的结果都变成空字符串!
6.异常映射(难)
另一个有趣的用例是利用 catchError()
操作符来覆盖抛出的异常:
exception.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from "@nestjs/common";
import { Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(catchError((err) => throwError(new BadGatewayException())));
}
}
7.Stream 重写(难)
有时我们可能希望完全阻止调用处理程序并返回不同的值 (例如, 由于性能问题而从缓存中获取), 这是有多种原因的。一个很好的例子是缓存拦截器,它将使用一些 TTL 存储缓存的响应。不幸的是, 这个功能需要更多的代码并且由于简化, 我们将仅提供简要解释主要概念的基本示例。
cache.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable, of } from "rxjs";
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
这是一个 CacheInterceptor
,带有硬编码的 isCached
变量和硬编码的响应 []
。我们在这里通过 of
运算符创建并返回了一个新的流, 因此路由处理程序根本不会被调用。当有人调用使用 CacheInterceptor
的端点时, 响应 (一个硬编码的空数组) 将立即返回。为了创建一个通用解决方案, 您可以利用 Reflector
并创建自定义修饰符。反射器 Reflector
在守卫章节描述的很好。
8.更多操作符(处理路由请求超时)
使用 RxJS
运算符操作流的可能性为我们提供了许多功能。让我们考虑另一个常见的用例。
**假设您要处理路由请求超时。如果您的端点在一段时间后未返回任何内容,则您将以错误响应终止。**以下构造可实现此目的:
timeout.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
RequestTimeoutException,
} from "@nestjs/common";
import { Observable, throwError, TimeoutError } from "rxjs";
import { catchError, timeout } from "rxjs/operators";
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000), //5s
catchError((err) => {
if (err instanceof TimeoutError) {
return throwError(new RequestTimeoutException());
}
return throwError(err);
})
);
}
}
5 秒后,请求处理将被取消。您还可以在抛出之前添加自定义逻辑RequestTimeoutException
(例如,释放资源)。
可以设置为全局拦截器!