Skip to content

NestJS 核心基础

bash
$ 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 很像)

image-20240310184557530

注意:目录结构会决定请求路由!

2.controller 写法

jsx
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() 构建响应

示例代码:

jsx
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.路由通配符

路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。

ts
@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

路由路径 'ab*cd' 将匹配 abcdab_cdabecd 等。字符 ?+* 以及 () 是它们的正则表达式对应项的子集。连字符(-) 和点(.)按字符串路径逐字解析。

6.状态码

如上所述,默认情况下,响应的状态码总是默认为 200,除了 POST 请求(默认响应状态码为 201)。

我们可以通过在处理函数外添加 @HttpCode(...) 装饰器来轻松更改此行为。

typescript
//HttpCode 需要从 @nestjs/common 包导入。
@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

通常,状态码不是固定的,而是取决于各种因素。在这种情况下,您可以使用 response (通过 @Res()注入 )对象(或者在出现错误时,抛出异常)。

7.Header

要指定自定义响应头,可以使用 @header() 装饰器

jsx
//Header 需要从 @nestjs/common 包导入。
@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

8.重定向

要将响应重定向到特定的 URL,可以使用 @Redirect() 装饰器或特定于库的响应对象(并直接调用 res.redirect())。

@Redirect() 装饰器有两个可选参数,urlstatusCode。 如果省略,则 statusCode 默认为 302

jsx
@Get()
@Redirect('https://nestjs.com', 301)

9.路由参数 get

当您需要接受动态数据(dynamic data)作为请求的一部分时(例如,使用GET /cats/1

以这种方式声明的路由参数可以使用 @Param() 装饰器访问,该装饰器应添加到函数签名中。

jsx
//Param 需要从 @nestjs/common 包导入。
@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

您还可以将特定的参数标记传递给装饰器,然后在方法主体中按参数名称直接引用路由参数。

jsx
@Get(':id')
findOne(@Param('id') id): string {
  return `This action returns a #${id} cat`;
}
query 参数 get
jsx
@Get()
findAll(@Query() query: ListAllEntities) {
	return `This action returns all cats (limit: ${query.limit} items)`;
}

get 查询中路由参数和查询参数(query 参数)的区别:

在 Web 开发中,路由参数和查询参数是两种常见的 URL 参数传递方式,它们有一些区别:

  1. 路由参数(Route Parameters)

    • 路由参数是将数据作为 URL 的一部分进行传递的一种方式。
    • 在 URL 中,路由参数通常出现在路径的一部分,并用于标识资源或指定特定的操作。
    • 例如,在 RESTful API 中,可以使用路由参数来指定资源的 ID 或标识要执行的特定操作。
    • 路由参数可以通过 URL 的路径部分进行访问,但不会在查询字符串中显示。
    • 在 Express.js 等 Web 框架中,可以通过定义路由路径中的参数来访问路由参数。

    示例:

    bash
    GET /users/:userId
    GET /posts/:postId/comments/:commentId
  2. 查询参数(Query Parameters)

    • 查询参数是将数据作为键值对形式添加到 URL 的查询字符串部分的一种方式。
    • 在 URL 中,查询参数通常跟在问号(?)之后,并使用等号(=)将键和值连接起来,多个参数之间使用和号(&)分隔。
    • 查询参数用于向服务器传递附加的数据,例如过滤、排序或限制结果等。
    • 查询参数可以在 URL 中以键值对的形式轻松识别和解析。
    • 在 Express.js 等 Web 框架中,可以通过解析请求对象中的查询参数来访问查询参数。

    示例:

    bash
    GET /users?role=admin&status=active
    GET /search?q=query&page=1&limit=10

总的来说,路由参数用于标识资源或指定操作,而查询参数用于向服务器传递附加的数据。它们在 URL 中的位置和使用方式略有不同,因此在设计 API 时需要根据需求选择合适的参数传递方式。

10.请求负载 post

此前我们列举的的 POST 路由处理程序样例中,处理程序没有接受任何客户端参数。我们在这里通过添加 @Body() 参数来解决这个问题。

首先(如果您使用 TypeScript),我们需要确定 DTO(数据传输对象)模式。DTO是一个对象,它定义了如何通过网络发送数据。

现在,我们来创建 CreateCatDto 类:

jsx
/*
  create-cat.dto.ts
*/
export class CreateCatDto {
  readonly name: string;
  readonly age: number;
  readonly breed: string;
}

然后,我们可以在 CatsController 中使用新创建的DTO

jsx
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

11.异步性

每个异步函数都必须返回一个 Promise。这意味着您可以返回延迟值,而 Nest 将自行解析它。

jsx
@Get()
async findAll(): Promise<any[]> {
  return []; //async包裹的函数会自动返回Promise
}

这是完全有效的。此外,通过返回 RxJS observable 流,Nest 路由处理程序将更加强大。 Nest 将自动订阅下面的源并获取最后发出的值(在流完成后)。

jsx
@Get()
findAll(): Observable<any[]> {
  return of([]);
}

上述的两种方法都是可行的,你可以选择你喜欢的方式。

12.完整示例

jsx
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 数组的原因。

jsx
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() 来装饰这个类 。

jsx
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;
}

创建服务类的命令:

bash
nest g service cats

现在我们有一个服务类来检索 cat ,让我们在 CatsController 里使用它 :

jsx
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 功能,管理依赖项非常容易,因为它们仅按类型进行解析。在下面的示例中,NestcatsService 通过创建并返回一个实例来解析 CatsService(或者,在单例的正常情况下,如果现有实例已在其他地方请求,则返回现有实例)。解析此依赖关系并将其传递给控制器的构造函数(或分配给指定的属性):

jsx
constructor(private readonly catsService: CatsService) {}
2.作用域

Provider 通常具有与应用程序生命周期同步的生命周期(“作用域”)。在启动应用程序时,必须解析每个依赖项,因此必须实例化每个提供程序。同样,当应用程序关闭时,每个 provider 都将被销毁。但是,有一些方法可以改变 provider 生命周期的请求范围。

3.自定义提供者

Nest 有一个内置的控制反转("IoC")容器,可以解决 providers 之间的关系。此功能是上述依赖注入功能的基础,但要比上面描述的要强大得多。

@Injectable() 装饰器只是冰山一角, 并不是定义 providers 的唯一方法。相反,您可以使用普通值、类、异步或同步工厂。看看这里找到更多的例子。

4.可选提供者

有时,您可能需要解决一些依赖项。例如,您的类可能依赖于一个配置对象,但如果没有传递,则应使用默认值。在这种情况下,关联变为可选的, provider 不会因为缺少配置导致错误。

要指示 provider 是可选的,请在 constructor 的参数中使用 @Optional() 装饰器。

比如这里 HttpService 需要注入相应的自定义 provider:HTTP_OPTIONS

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

jsx
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 数组中。

jsx
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 类的依赖关系(需要的服务)。这就是我们目前的目录结构:

jsx
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 用它来组织应用程序结构。

image-20240311144916229

每个 Nest 应用程序至少有一个模块,即根模块。在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能

@module() 装饰器接受一个描述模块属性的对象:

providers由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
controllers必须创建的一组控制器
imports导入模块的列表,这些模块导出了此模块中所需提供者
exports由本模块提供并应在其他模块中可用的提供者的子集。

默认情况下,该模块封装提供程序。这意味着无法注入既不是当前模块的直接组成部分(模块的本身的程序),也不是从导入的模块导出的提供程序(导入进来的模块的暴露的程序)。

从模块导出的提供程序被视为模块的公共接口或 API

2.功能模块

CatsControllerCatsService 属于同一个应用程序域。 应该考虑将它们移动到一个功能模块下,即 CatsModule

jsx
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

jsx
import { Module } from "@nestjs/common";
import { CatsModule } from "./cats/cats.module";

@Module({
  imports: [CatsModule],
})
export class ApplicationModule {}

注:模块基本目录结构

现在 Nest 知道除了 ApplicationModule 之外,注册 CatsModule 也是非常重要的。 这就是我们现在的目录结构:

jsx
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.共享模块

image-20240311150224940

在 Nest 中,默认情况下,模块是单例,因此您可以轻松地在多个模块之间共享同一个提供者实例(只需要每个模块都引入那个单例的共享模块即可)。

实际上,每个模块都是一个共享模块。一旦创建就能被任意模块重复使用。假设我们将在几个模块之间共享 CatsService 实例。 我们需要把 CatsService 放到 exports 数组中,如下所示:

jsx
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.模块导出

模块可以导出他们的内部提供者。 而且,他们可以再导出自己导入的模块。

jsx
@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

这样别的模块导入了 CoreModule 就相当于同时导入了 CoreModule 和 CommonModule

5.依赖注入

提供者也可以注入到模块(类)中(例如,用于配置目的):

jsx
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,数据库连接等等。这就是为什么你能够使模块成为全局模块

jsx
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

jsx
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() 装饰器元数据中)定义提供程序,但此外-根据传递给方法的 entitiesoptions 对象 forRoot() -公开提供程序的集合,例如存储库。

请注意,动态模块返回的属性扩展(而不是覆盖)@Module() 装饰器中定义的基本模块元数据(这样 Module 本身也可以定义一些静态的内容)。这就是从模块导出静态声明的 Connection 提供程序和动态生成的存储库提供程序的方式。

如果要在全局范围内注册动态模块,请将 global 属性设置为 true

jsx
return {
  global: true,
  module: DatabaseModule,
  providers: providers,
  exports: providers,
};

上面的动态模块 DatabaseModule 可以被导入,并且被配置以下列方式:

jsx
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() 在导出数组中省略方法调用:

jsx
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 的变量表示。

image-20240311170723588

中间件也就是位于客户端和 controller 路由响应之间的部分,就是中间件处理的部分!(可以影响请求对象和响应对象)

Nest 中间件实际上等价于 express 中间件。 下面是 Express 官方文档中所述的中间件功能:

中间件函数可以执行以下任务:

  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求-响应周期。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。

logger.middleware.ts

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() 可接受一个字符串、多个字符串、对象、一个控制器类甚至多个控制器类。在大多数情况下,您可能只会传递一个由逗号分隔的控制器列表。以下是单个控制器的示例:

jsx
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() 方法可以使用单个中间件,也可以使用多个参数来指定多个多个中间件

jsx
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);

有时我们想从应用中间件中排除某些路由。我们可以使用该 exclude() 方法轻松排除某些路由。此方法可以采用一个字符串,多个字符串或一个 RouteInfo 对象来标识要排除的路由,如下所示:

jsx
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 层上。

jsx
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来引用所需的请求方法类型

jsx
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.路由通配符

路由同样支持模式匹配。例如,星号被用作通配符,将匹配任何字符组合。

jsx
forRoutes({ path: "ab*cd", method: RequestMethod.ALL });

以上路由地址将匹配 abcdab_cdabecd 等。字符 ?+* 以及 () 是它们的正则表达式对应项的子集。连字符 (-) 和点 (.) 按字符串路径解析。

5.函数式中间件

我们使用的 LoggerMiddleware 类非常简单。它没有成员,没有额外的方法,没有依赖关系。为什么我们不能只使用一个简单的函数?这是一个很好的问题,因为事实上 - 我们可以做到。这种类型的中间件称为函数式中间件。让我们把 logger 转换成函数。

logger.middleware.ts

jsx
export function logger(req, res, next) {
  console.log(`Request...`);
  next();
}

现在在 AppModule 中使用它。

jsx
consumer.apply(logger).forRoutes(CatsController);

当您的中间件没有任何依赖关系时,我们可以考虑使用函数式中间件。

注意:这并没有改变中间件的级别,它依然是比 service 高一级的!

6.全局中间件

如果我们想一次性将中间件绑定到每个注册路由,我们可以使用由INestApplication实例提供的 use()方法:

typescript
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);

5.Pipe 管道

1.基本介绍

注意:管道也是一种提供者 providers,主要作用于 controller

管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。

image-20240311183155984

管道有两个典型的应用场景:

  • 转换:管道将输入数据转换为所需的数据输出(例如,将字符串转换为整数)
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常

在这两种情况下, 管道 参数(arguments) 会由 控制器(controllers)的路由处理程序 进行处理。Nest 会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行转换或是验证处理,然后用转换好或是验证好的参数调用原方法。

Nest 自带很多开箱即用的内置管道。你还可以构建自定义管道。

2.内置管道

Nest 自带九个开箱即用的管道,即

验证管道:

  • ValidationPipe

转换管道:

  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe

我们先来快速看看如何使用ParseIntPipe。这是一个转换的应用场景,管道确保传给路由处理程序的参数是一个整数(若不是整数,就进行转换;若转换失败,则抛出异常)。

我们传递了一个类(ParseIntPipe),而不是一个实例,将实例化留给框架去处理,做到了依赖注入

jsx
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
  return this.catsService.findOne(id);
}

这确保了我们在 findOne() 方法中接收的参数是一个数字(与 this.catsService.findOne() 方法的诉求一致),或者在路由处理程序被调用之前抛出异常。

举个例子,假设路由是这样子的

bash
GET localhost:3000/abc

Nest 将会抛出这样的异常:

jsx
{
  "statusCode": 400,
  "message": "Validation failed (numeric string is expected)",
  "error": "Bad Request"
}

这个异常阻止了 findOne() 方法的执行。

使用 ParseUUIDPipe 解析字符串并验证是否为 UUID 的例子

typescript
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
  return this.catsService.findOne(uuid);
}

对于管道和守卫,我们也可以选择传递一个实例。如果我们想通过传递选项来自定义内置管道的行为,传递实例很有用:

jsx
@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

jsx
import { PipeTransform, Injectable, ArgumentMetadata } from "@nestjs/common";

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

parse-int.pipe.ts

jsx
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 是当前处理的方法参数的元数据。元数据对象具有以下属性:

typescript
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.对象结构验证

让我们把验证管道变得更有用一点。仔细看看 CatsControllercreate() 方法,我们希望在该方法被调用之前,请求主体(post body)得到验证

jsx
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

有几种方法可以实现。一种常见的方式是使用基于结构的验证。我们来尝试一下。

1.Joi 库(简单)

Joi 库允许使用可读的 API 以直接的方式创建 schema,让我们构建一个基于 Joi schema 的验证管道。

首先安装依赖:

bash
$ npm install --save joi
$ npm install --save-dev @types/joi

在下面的代码中,我们先创建一个简单的 class,在构造函数中传递 schema 参数。然后我们使用 schema.validate() 方法验证参数是否符合提供的 schema。

jsx
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

  1. 创建一个 JoiValidationPipe 实例
  2. 传递上下文特定的 Joi schema 给构造函数
  3. 绑定到方法
jsx
//从 @nestjs/common 包导入 @UsePipes() 装饰器
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema)) //createCatSchema是什么样子的?
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
jsx
export class CreateCatDto {
  name: string; //后面的string不是值,而是类型声明
  age: number;
  breed: string;
}
2.类验证器(强大)(难)

本节中的技术需要 TypeScript ,如果您的应用是使用原始 JavaScript编写的,则这些技术不可用。

让我们看一下验证的另外一种实现方式。

Nest 与 class-validator 配合得很好。这个优秀的库允许您使用基于装饰器的验证。装饰器的功能非常强大,尤其是与 Nest 的 Pipe 功能相结合使用时,因为我们可以通过访问 metatype 信息做很多事情,在开始之前需要安装一些依赖。

bash
$ npm i --save class-validator class-transformer

安装完成后,我们就可以向 CreateCatDto 类添加一些装饰器。在这里,我们看到了这种技术实现的一个显著优势:CreateCatDto 类仍然是我们的 Post body 对象的单一可靠来源(而不是必须创建一个单独的验证类)。

create-cat.dto.ts

jsx
import { IsString, IsInt } from "class-validator";

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}

现在我们来创建一个 ValidationPipe 类。

validate.pipe.ts

jsx
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

jsx
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();

混合应用useGlobalPipes() 方法不会为网关和微服务设置管道, 对于标准(非混合) 微服务应用使用 useGlobalPipes() 全局设置管道。

但是,从任何模块外部注册的全局管道(即使用了 useGlobalPipes(), 如上例所示)无法注入依赖,因为它们不属于任何模块。为了解决这个问题,可以使用以下构造直接为任何模块设置管道:

jsx
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 实体,封装成公共管道,是有很大的作用的!可以减少代码!

typescript
@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,如下所示:

jsx
@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!和中间件、管道所处的位置也基本一样!

image-20240311212226978

拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:

  • 在函数执行之前/之后绑定额外的逻辑
  • 转换从函数返回的结果
  • 转换从函数抛出的异常
  • 扩展基本函数行为
  • 根据所选条件完全重写函数 (例如, 缓存目的)

就是各种玩弄操作函数!

2.参数

每个拦截器都有 intercept() 方法,它接收 2 个参数。

1.ExecutionContext

第一个是 ExecutionContext 实例(与守卫完全相同的对象)。 ExecutionContext 继承自 ArgumentsHostArgumentsHost传递给原始处理程序的参数的一个包装,它根据应用程序的类型包含不同的参数数组。你可以在这里读更多关于它的内容(在异常过滤器章节中)。

通过扩展 ArgumentsHostExecutionContext 还添加了几个新的帮助程序方法,这些方法提供有关当前执行过程的更多详细信息。这些详细信息有助于构建可以在广泛的控制器,方法和执行上下文中使用的更通用的拦截器。ExecutionContext此处了解更多信息。

2.CallHandler

第二个参数是 CallHandler。如果不手动调用 handle() 方法,则主处理程序根本不会进行求值。这是什么意思?基本上,CallHandler是一个包装执行流的对象,因此推迟了最终的处理程序执行

比方说,有人提出了 POST /cats 请求。此请求指向在 CatsController 中定义的 create() 处理程序。如果在此过程中未调用拦截器的 handle() 方法,则 create() 方法不会被计算。只有 handle() 被调用(并且已返回值),最终方法才会被触发。为什么?因为 Nest 订阅了返回的流,并使用此流生成的值来为最终用户创建单个响应或多个响应。而且,handle() 返回一个 Observable,这意味着它为我们提供了一组非常强大的运算符,可以帮助我们进行例如响应操作。

3.截取切面

使用拦截器在函数执行之前或之后添加额外的逻辑

当我们要记录与应用程序的交互时,它很有用,例如 存储用户调用,异步调度事件或计算时间戳。作为一个例子,我们来创建一个简单的例子 LoggingInterceptor

logging.interceptor.ts

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

ts
// @UseInterceptors() 装饰器从 @nestjs/common 导入。

@UseInterceptors(LoggingInterceptor)
export class CatsController {}

由此,CatsController 中定义的每个路由处理程序都将使用 LoggingInterceptor。当有人调用 GET /cats 端点时,您将在控制台窗口中看到以下输出:

jsx
Before...
After... 1ms

请注意,我们传递的是 LoggingInterceptor 类型而不是实例,让框架承担实例化责任并启用依赖注入。另一种可用的方法是传递立即创建的实例:和中间件同理

jsx
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}

4.全局拦截器

如上所述, 上面的构造将拦截器附加到此控制器声明的每个处理程序。如果我们决定只限制其中一个, 我们只需在方法级别设置拦截器。为了绑定全局拦截器, 我们使用 Nest 应用程序实例的 useGlobalInterceptors() 方法:

jsx
const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());

全局拦截器用于整个应用程序、每个控制器和每个路由处理程序。在依赖注入方面, 从任何模块外部注册的全局拦截器 (如上面的示例中所示) 无法插入依赖项, 因为它们不属于任何模块。为了解决此问题, 您可以使用以下构造直接从任何模块设置一个拦截器: 和全局管道同理

jsx
import { Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
})
export class AppModule {}

另一种选择是使用执行上下文功能。另外,useClass 并不是处理自定义提供商注册的唯一方法。在这里了解更多。

5.响应映射

我们已经知道, handle() 返回一个 Observable。此流包含从路由处理程序返回的值, 因此我们可以使用 map() 运算符轻松地对其进行改变。

简单来说,就是把函数的返回值(比如请求响应)进行相应的修改映射!

注意:响应映射功能不适用于特定于库的响应策略(禁止直接使用 @Res() 对象)。

让我们创建一个 TransformInterceptor, 它将打包响应并将其分配给 data 属性。

transform.interceptor.ts

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 值转换为空字符串 ''。我们可以使用一行代码并将拦截器绑定为全局代码。由于这一点,它会被每个注册的处理程序自动重用。

jsx
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

typescript
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

typescript
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

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(例如,释放资源)。

可以设置为全局拦截器!