Skip to content

NestJS 框架介绍

NestJS 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的框架。它利用 TypeScript 来开发强类型、可扩展的应用程序。NestJS 深受 Angular 框架的启发,因此它的架构、模块化和依赖注入系统与 Angular 有许多相似之处。以下是 NestJS 的一些核心特性和概念:

  1. 依赖注入(DI):使用了一个强大的依赖注入系统,这是控制反转(IoC)容器的一个实现。依赖注入允许你将服务(例如数据库连接工具库等)注入到你的控制器、守卫、拦截器和服务中。
  2. 控制器:控制器处理 HTTP 请求和响应。它们通常用来定义路由和处理逻辑
  3. 服务:服务是应用程序的业务逻辑层,通常用于封装和重用逻辑,可以被控制器或其他服务调用。
  4. 守卫(Guards):守卫是特殊的类,可以用来预处理请求,例如身份验证授权
  5. 拦截器(Interceptors):拦截器用于拦截处理流程,可以在调用控制器之前或之后执行代码,例如日志记录错误处理
  6. 装饰器:广泛使用 JS 的装饰器,它被用于类和方法上,提供了一种强大的方式来添加额外信息行为
  7. 中间件:支持 ExpressFastify 等 HTTP 平台的中间件,允许你在应用程序中使用现有的中间件
  8. 插件和钩子:提供了生命周期钩子,允许你在应用程序的不同阶段执行代码。
  9. 微服务支持:支持构建微服务架构,提供了与消息传递系统(如 MQTTWebSockets)集成的工具。
  10. CQRS 和事件源:支持命令查询责任分离(CQRS)和事件源模式,这对于构建复杂的业务逻辑和数据一致性非常有用。
  11. 开箱即用:内置了许多开箱即用的解决方案,如数据库访问层文件上传处理缓存等。

NestJS 的设计理念是提供一个完整的解决方案,让开发者可以专注于构建业务逻辑,而不是花时间在底层架构上。

入口main.ts

ts
async function bootstrap() {
	const app = await NestFactory.create(AppModule)
  
  // 可通过 app.use 添加全局配置
  app.use(logger) // 全局 logger 中间件
  app.useGlobalFilters(new HttpExceptionFilter()) // 全局异常过滤器
  app.useGlobalPipes(new ValidationPipe()) // 全局管道处理
  app.useGlobalGuards(new RolesGuards()) // 全局守卫
  app.useGlobalInterceptors(new LoggingInterceptor()) // 全局拦截器
  
  await app.listen(3000)
}

Nestjs的CLI快捷命令

NestJS 提供了许多实用的 CLI(命令行接口)命令来简化项目的设置、管理和开发过程。以下是一些常用的快捷命令

命令别名缩写描述
nest generate resource RESOURCE-NAMEnest g res RESOURCE-NAME将创建一个新的资源包含CRUD及控制器、服务、模块
nest generate module MODULE-NAMEnest g mo MODULE-NAME将创建一个新的模块(目录)以及相关的 module.ts 文件
nest generate controller CONTROLLER-NAMEnest g co CONTROLLER-NAME生成控制器,可以使用 --no-spec 标志来避免生成测试文件。这个命令会创建一个新的控制器文件和基本的路由处理方法。
nest generate service SERVICE-NAMEnest g s SERVICE-NAME生成一个新的服务类,服务通常用于封装应用逻辑。
nest generate guard GUARD-NAMEnest g gu GUARD-NAME创建用于定义应用层的安全规则的守卫
nest generate interceptor INTERCEPTOR-NAMEnest g in INTERCEPTOR-NAME拦截器可以修改方法执行的行为,如响应、请求处理前后执行特定的逻辑。
nest generate filter FILTER-NAMEnest g f FILTER-NAME过滤器允许你处理异常或请求,以及发送自定义的响应给客户端。
nest generate pipe PIPE-NAMEnest g pi PIPE-NAME管道是处理数据的另一种方式,可以自动转换输入到路由处理器或从路由处理器转换输出。

--flat 和 --no-flat 是指定是否生成对应目录的

--spec 和 --no-spec 是指定是否生成测试文件

--skip-import 是指定不在 AppModule 里引入

下面是一些常用的 Nest CLI 命令及其解释:

  1. nest new :创建一个新的 Nest 应用程序。 参数是你想要为应用程序指定的名称。
  2. nest build:将 Nest 应用程序构建为可执行的 JavaScript 代码。这将把你的 TypeScript 代码编译为 JavaScript,以便在生产环境中运行。
  3. nest start:启动 Nest 应用程序的开发服务器。在开发过程中使用此命令可以自动检测更改并重新加载应用程序。
  4. nest generate :生成不同类型的 Nest 代码文件,如控制器、服务、模块等。 参数是你想要生成的文件类型, 参数是文件的名称。
  5. nest generate module :生成一个新的模块文件。模块是 Nest 应用程序的组织单元,用于将相关的功能模块化。
  6. nest generate controller :生成一个新的控制器文件。控制器负责处理传入的请求并返回响应。
  7. nest generate service :生成一个新的服务文件。服务用于处理应用程序的业务逻辑。
  8. nest generate class :生成一个新的类文件。类是一种可复用的代码块,用于定义对象的行为和属性。
  9. nest generate pipe :生成一个新的管道文件。管道用于对传入的数据进行转换和处理。
  10. nest generate guard :生成一个新的守卫文件。守卫用于在路由处理程序执行之前对请求进行身份验证和授权。

Nestjs的控制器Controller

控制器(Controller)是MVC架构中C的实现,它负责接收请求(Requests)和发出响应(Responses)。

在nest里,controller的作用就是一个单路由,类型是一个装饰器。假设你用过express/koa等框架,那么你一定对app.get/post/update/delete等API很熟悉。如果想要去请求/list页面的资源,在express会这样写:

ts
app.get('/list', middleware, (req, res) => {
 //回调函数用来处理逻辑
})

@Controller() 装饰器中使用路径前缀可以使我们轻松地对一组相关的路由进行分组,并最大程度地减少重复代码。在nest里controller的底层就是对express的封装,看看封装后的controller.ts文件的代码怎么请求/list页面的资源:

ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

// @Controller()装饰AppController类
@Controller()
export class AppController {
  // constructor(private readonly appService: AppService) {}
  
  // @Get()装饰getHello方法
  @Get('list')
  getHello(): string {
    //return this.appService.getHello();
    return 'hello nestjs!';
  }
}

@Controller()里的参数不填默认就是'/'@Get('list')在Get装饰器里传入地址,在每次client端对/list进行访问时都会执行getHello函数。所以这个时候访问localhost:3000/list页面上会有hello world!。

如果想进行其他请求,继续在页面下写;

想要middleware约束请求时引入限制的装饰器进行装饰即可;

想要req/res参数,注入即可;

ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import Request from 'express'

// @Controller()装饰AppController类
@Controller()
export class AppController {
  // constructor(private readonly appService: AppService) {}
  
  // @Get()装饰getHello方法
  @Get('list')
  // @Req去装饰参数request
  // 为了用到express里request的代码提示还需要导入express中Request类去定义request
  getHello(@Req() request: Request): string {
    //return this.appService.getHello();
    return 'hello nestjs!';
  }
  
  @Post()
  postHello(): object {
    return { succuss: true };
  }
}

Request Object 请求对象 参数

处理函数通常需要接受来自客户端的请求细节。Nest提供了底层请求对象的接口(默认是Express)。我们可以通过在函数签名中添加 @Req() 装饰器来获取请求对象

ts
// cats.controller.ts
import { Controller, Get, Req } from '@nestjs/common'
import { Request } from 'express'

@Controller('cats')
export class CatsController {
  @Get('cats')
  findAll(@Req() request: Request): string {
    return '这个动作返回所有的cats'
  }
}

该请求对象代表了HTTP请求,并且拥有查询字符串,HTTP请求头,和请求体等属性。大多数情况下不需要手动获取这些属性,而是直接使用装饰器比如 @Body() 或者 @Query() 。下面是提供的装饰器以及他们所代表的相应的对象:

@Request()req
@Response(), @Res()*res
@Next()next
@Session()req.session
@Param(key?: string)req.params / req.params[key]
@Body(key?: string)req.body / req.body[key]
@Query(key?: string)req.query / req.query[key]
@Headers(name?: string)req.headers / req.headers[name]
@Ip()req.ip

Route parameters 路由参数

当需要接受请求路径中一部分作为动态数据时,静态路径做不到。(例如GET /cats/1获取到id为1的cat)。定义带有参数的路由,只需要添加参数标识就可以在请求url中捕获动态的值。然后就可以在函数签名中使用@Param()访问到路由参数。

ts
@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `这个动作会返回id为${params.id}的cat`
}

我们可以接受一个id作为参数来获取到params.id

ts
@Get('id')
findOne(@Param('id') id): string {
  return `这个动作会返回id为${id}的cat`
}

Asynchronicity 异步

Nest支持异步操作async/await。异步函数会返回一个Promise。

数据的获取是异步的,Nest支持async函数 每个异步的函数返回一个Promise,这意味着你可以返回一个延迟的值,Nest可以自己处理。

ts
// cats.controller.ts
@Get()
async findAll(): Promise<any[]> {
    return []
}

上面的代码完全有效,Nest路由处理函数甚至可以返回RxJs(可观测的数据流)。Nest将自动订阅资源,并流完成时获取最后一次发出的值。

ts
// cats.controller.ts
@Get()
findAll(): Observable<any[]> {
    return of([]);
}

控制器的运行

控制器定义好之后,Nest并不知道这个控制器的存在,并且也不会创建实例。 控制器通常属于一个模块Module,需要以数组的形式传进装饰器@Module()。

ts
import { Module } from 'nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
    controllers: [CatsController],
})
export class AppModule {}

Nesjts的Provider

  • 控制反转 IOC 模式中的依赖注入特性
  • 使用 @Injectable() 装饰
  • 通常只关注于处理业务逻辑,供 Controller 调用
  • nest g service cats

在前面创建Controller的时候,我们注释了app.controller.ts里的一段有关service的代码,现在我们将它加上来分析。

ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

// @Controller()装饰AppController类
@Controller()
export class AppController {
  //在constructor中注入appService依赖
  constructor(private readonly appService: AppService) {}
  
  // @Get()装饰getHello方法
  @Get('list')
  getHello(): string {
    return this.appService.getHello();
    //return 'hello nestjs!';
  }
}

首先,需要先理解为什么这样用:假设我们直接在getHello函数里实现业务功能,那么这个函数里的代码可能会越堆越多,并且如果函数中一些业务代码可以在别的地方复用的话,那在别的地方用的时候还要到这个文件copy一份,这样就导致Controller控制器的功能不再单纯变得混乱而且重复代码也不友好,所以此时我们需要将在控制器中需要实现的复杂业务功能隔离出来,这就是service文件的作用。

理解了原因,接下来就是怎么做。其实做法很多,只要把业务逻辑写在别的地方然后引入到Controller里就行了,但是怎么引入是最优的呢?nestjs选择了用一种名为依赖注入(DI) 的设计模式来引入。

我们来看一下生成的app.service.ts的示例代码:

ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

可以看到service里的代码和Controller组件的代码区别就在于Injectable()装饰器,这个装饰器就是为了实现依赖注入(DI) 的设计模式的一种封装。在Service里实现有关于getHello的业务逻辑,Controller控制器需要依赖这个功能那么就通过constructor去注入这个依赖。

那么至此可以看出Provider(提供者)这个角色的作用和名字差不多,就是一个提供依赖给别的组件或者模块的角色,就是一个用 @Injectable() 装饰器注释的类,所以在nestjs里的很多类都可以被当做成提供者,只要你的作用是给别人提供依赖,比如service, repository, factory, helper 等等。 他们都可以通过 constructor 注入 依赖关系。

OK,我们再次返回代码中。此时我们已经定义了一个提供者app.service.ts,它将提供服务给app.controller.ts控制器使用,这时我们还需要在module里注册这个service才可以成功完成注入。那么,在app.module.ts里进行注册:

TypeScript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

到这里,目录结构没有解释的就还差module了。

本节小结:Provider是一个用 @Injectable() 装饰器注释的类,用来提供服务。

Nestjs的Module

通过上面的 app.module.ts,我们可以知道 Modules 是用于封装和组织相关importscontrollersproviders的容器。这个文件就是相当于前端的某个页面模块容器。

每个模块都可以定义以下部分:

  • controllers:处理传入请求并返回响应的组件。
  • providers:封装业务逻辑和数据的组件,通常是服务。
  • imports:当前模块需要依赖的其他模块列表。
  • exports:当前模块希望导出以便其他模块使用的提供者(服务、组件等)列表。

每个 nest 应用程序至少有一个模块,即根模块,也就是我们现在在代理看到的app.module.ts文件。实际的项目肯定是由很多个模块组成的,每一个模块都会实现一组属于自己模块的功能,比如注册模块。

下面是@Module()装饰器接受描述模块属性的对象的含义:

image_3.png

在写代码的时候,我们经常都会听到模块化这个词语。对于写JavaScript的同学,说起模块化我们会想到ESModule,但是ESModule并不是设计上真正意义的模块化,前端也并没有对模块化有特别好的示范。对于nestjs里的Module,我觉得就是Typescript语言对模块化的一个最佳示范吧。

最后我们用一个图为nestjs默认产生的模块的作用做一个总结:

image_4.png

  • exports:导出由本模块提供,并且需要在其他模块中使用的 Provider。
  • imports:导入模块,这些模块导出的 Provider 可以在本模块中使用。

具体的来讲: 模块为其中 Controller、Provider 这些组件提供执行范围。模块中注册的 Provider 对模块的其他成员可见,但当它需要在模块外部可见时,需要从模块中导出,并在其他消费模块导入。

比如,DogModule 中注册的 DogService 可以在 DogController 中使用,当它需要在 CatController 中使用时,需要把它放到 DogModule 中 @Moduleexports 数组中,然后在 CatModule 中 imports DogModule。

import { Module } from '@nestjs/common';
import { DogService } from './dog.service';
import { DogController } from './dog.controller';

@Module({
  providers: [DogService],
  controllers: [DogController],
  exports: [DogService],
})
export class DogModule {}
import { Module } from '@nestjs/common';
import { DogService } from 'src/dog/dog.service';
import { CatController } from './cat.controller';

@Module({
  imports: [DogService],
  controllers: [CatController],
})
export class CatModule {}

Nestjs的Module笔记二

  • 每个应用至少有一个模块,作为应用的入口
  • 使用 @Module 装饰
  • nest g module cats

模块指的是使用@Module装饰器修饰的类,每个应用程序至少有一个模块,即根模块。根模块是Nest用于构建应用程序的起点,理论上Nest程序可能只有根模块,但在大多数情况下是存在多个模块的,每个模块各自封装一组相关的功能。

5-1.png

@Module装饰器

@Module()装饰器可以传入一个对象,属性值如下:

providers将由 Nest 注入器实例化的提供程序,并且至少可以在该模块中共享
controllers该模块中定义的必须实例化的控制器集
imports导入模块的列表,导出该模块所需的提供程序
exports该子集providers由该模块提供,并且应该在导入该模块的其他模块中可用
ts
@Module({
  imports: [NanjiuModule, UserModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

模块的共享

如果你想把当前模块的service暴露给其它模块使用,则可以使用exports导出该服务

比如我使用nest g resource info新建了一个info类,并且使用export导出该服务

ts
// info.module.ts
import { Module } from '@nestjs/common';
import { InfoService } from './info.service';
import { InfoController } from './info.controller';

@Module({
  controllers: [InfoController],
   // 依赖注入,在控制器中自动实例化该服务
  // providers: [AppService, UserService],
  providers: [InfoService], // 提供者
  exports: [InfoService] // 导出 InfoService 供其他模块使用
})
export class InfoModule {}

然后我在user模块中使用imports导入该模块

ts
// user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { InfoModule } from 'src/info/info.module';

@Module({
  imports: [InfoModule], // 导入 InfoModule
  controllers: [UserController],
  providers: [UserService]
})
export class UserModule {}

最后在controller中依赖注入并使用

ts
// user.controller.ts
import { InfoService } from 'src/info/info.service';

@Controller('user')
export class UserController {
  constructor(
    private readonly userService: UserService,
    private readonly infoService: InfoService, // 注入 InfoService
    ) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.infoService.findAll() // 调用 InfoService 的 findAll 方法

    // return this.userService.create(createUserDto);
  }
  //...
}

模块再导出

可以把一些常用的,公共的模块,全部先import进一个CommonModule,然后再把它们从exprots全部导出,以后如果有那个模块想要使用其中某个模块的Service,只需要将这个CommonModule导入即可,不用再导入所有的依赖模块

ts
// common.module.ts
@Module({
  imports: [Module1, Module2, Module3, Module4],
  exports: [Module1, Module2, Module3, Module4],
})
export class CommonModule {}

全局模块

通过@Global()装饰器声明一个全局模块,只需要在根模块imports注册该全局模块,就可以在其他所有模块内使用它导出的Service

比如:将info声明为全局模块

ts
// info.module.ts
@Global() // 全局模块
@Module({
  controllers: [InfoController],
  providers: [InfoService], // 提供者
  exports: [InfoService] // 导出 InfoService 供其他模块使用
})
export class InfoModule {}

然后在user模块中无需导入,只需依赖注入就可直接使用(前提是已在根模块导入)

ts
// user.controller.ts
import { CreateUserDto } from './dto/create-user.dto';
import { InfoService } from 'src/info/info.service';

@Controller('user')
export class UserController {
  constructor(
    private readonly userService: UserService,
    private readonly infoService: InfoService, // 注入 InfoService
    ) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.infoService.findAll() // 调用 InfoService 的 findAll 方法
  }
}

动态模块

动态模块能够让我们创建可定制的模块,当导入模块并向其传入某些选项参数,这个模块根据这些选项参数来动态的创建不同特性的模块。

创建动态模块

动态模块其实就是给当前Module类提供一个forRoot方法,该方法返回一个新的Module,这个Module的类型是一个DynamicModule,在其他模块需要注册使用时,可以使用xxxModule.forRoot(args)来动态的注册不同的Module,以达到提供不同providers的目的。

这里我们创建一个config的动态模块

ts
// config.module.ts
import { Module, DynamicModule, Global } from '@nestjs/common';
import { NanjiuService } from 'src/nanjiu/nanjiu.service';
import { UserService } from 'src/user/user.service';

interface Options {
    name: string
}
@Global()
@Module({
})
export class ConfigModule {
    static forRoot(options: Options): DynamicModule {
        console.log('options', options)
        return {
            module: ConfigModule,
            providers: [
                {provide: 'config', useClass: options.name === 'nanjiu' ? NanjiuService : UserService},
            ],
            exports: [
                {provide: 'config', useClass: options.name === 'nanjiu' ? NanjiuService : UserService}
            ]
        }
    }
}

这个例子很简单,首先需要自己编写一个静态方法,该方法通过接收传递进来的参数判断使用哪一个service,并且为了方便,我这里直接使用@Global()装饰器将该模块声明称了全局模块

传递参数使用

调用静态方法传递参数

ts
// app.module.ts
@Module({
  imports: [ConfigModule.forRoot({name: 'fe'})],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

然后在controller中使用

ts
import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService, 
    @Inject('config') private readonly configService // 注入 ConfigService
    ) {}

  @Get('/hello2')
  get2() {
    return this.configService.getHello() // 调用 ConfigService 的 getHello 方法
  }
}

比如上面forRoot传递的参数是{name: 'nanjiu'},所以此时的ConfigModule注入的应该是UserService

修改forRoot参数之后

ts
// app.module.ts
@Module({
  imports: [ConfigModule.forRoot({name: 'nanjiu'})],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

此时通过get方式再访问同样的路由,应该是访问到NanjiuService提供的服务了。

Nestjs的请求链路流程

  • 客户端 发起请求开始,一系列的处理流程顺序
  • Client Side -> Middleware -> Guards -> Interceptors -> pipes -> [Controllers -> Providers]: 具体的执行函数
  • Exception Filter : 中间件、守卫、拦截器、管道以及函数执行时抛出的异常,执行顺序均会从抛出错误的那一刻转至异常拦截器

Nesjts的中间件

中间件是在路由处理程序之前调用的函数。中间件函数可以访问 requestresponse 对象,以及应用请求-响应周期中的 next() 中间件函数。下一个中间件函数通常由名为 next 的变量表示。

  • 函数,可以访问到请求响应对象
  • 通过 next() 将控制权传递给下一个中间件函数
  • Nest 可以通过函数或者类实现中间件
typescript
// 类的方式, 使用 @Injectable 装饰, 并实现 NestMiddleware 接口
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()
  }
}

中间件的使用

我们使用模块类的 configure() 方法设置中间件。包含中间件的模块必须实现 NestModule 接口。

typescript
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');
    }
}

此外,我们还可以通过在配置中间件时将包含路由 path 和请求 method 的对象传递给 forRoutes() 方法,进一步将中间件限制为特定的请求方法。

全局中间件

使用 use() 方法即可:

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

多个中间件

为了绑定顺序执行的多个中间件,只需在 apply() 方法中提供一个逗号分隔的列表:

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

Nestjs的守卫

  • 中间件逻辑之 后、拦截器/管道之 前 执行
  • 决定请求是否要被控制器处理,一般使用在权限、角色场景中
  • 相比于中间件(调用 next() 任务完成),守卫还需要关注上下游
ts
// 使用 @Injectable 装饰, 实现 CanActivate 接口
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Observable } from 'rxjs'

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true
  }
}

// 通过 @Guards(RolesGuard) 使用
  • 此时已经有用了守卫,但没有执行上下文
  • 应该有一些需要访问到的权限类型
  • 通常使用 @SetMetadata() 对控制器添加元数据
ts
@Post()
@SetMetadata('role', ['admin'])
async creat() { ... }

实现一个自定义装饰器

typescript
// 实现一个自定义装饰器
import { SetMetadata } from '@nestjs/common'

export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

// 使用
@Post()
@Roles('admin')
async create() { ... }

NestJS 提供了几种内置的 Guard

  • AuthGuard: 用于身份验证。它检查请求是否包含有效的认证凭据,并在验证失败时抛出异常。
  • RolesGuard: 用于基于角色访问控制。它确保只有具有特定角色的用户才能访问特定的路由。
  • PermissionGuard: 用于基于权限的访问控制。它确保用户具有执行特定操作的权限。
  • ThrottlerGuard: 用于速率限制。它限制用户在给定时间内可以发起的请求数量。
  • WsGuard: 用于 WebSocket 连接的守卫。它允许你在 WebSocket 连接建立之前执行一些逻辑。

新增中间件的参考案例1

在middleware文件夹中新增一个logger.middleware文件

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    const { method, path, query, body } = req;
    let params = query;
    if (method == 'POST') {
      params = body;
    }
    console.log(`${method} ${path} params values:${JSON.stringify(params)}`);
    next();
  }
}

在 app.module文件中使用

ts
import { Module,MiddlewareConsumer,RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CoffeesModule } from './coffees/coffees.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SvncatalogueModule } from './svncatalogue/svncatalogue.module';
import { UploadfileModule } from './uploadfile/uploadfile.module';
import {LoggerMiddleware} from './common/middleware/logger.middleware'

@Module({
  imports: [
    CoffeesModule,
    TypeOrmModule.forRoot(),
    SvncatalogueModule,
    UploadfileModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {
    configure(consumer: MiddlewareConsumer) {
    // 为路由添加中间件
    // consumer.apply(LoggerMiddleware).forRoutes(''); // 空串表示所有接口都会走该中间件,也可指定某路由path

    //指定projectManage controller的路由走该中间件
    consumer
      .apply(LoggerMiddleware)
      // 排除某接口的post请求,也支持配置多个,但是此处/halfMonthApi不可少
      // .exclude({ path: '/halfMonthApi/projectManage/closeProject', method: RequestMethod.POST })

      // 所有接口都会走该中间件
      // .forRoutes('');

      // 定projectManage controller的路由走该中间件
      // .forRoutes(ProjectManageController);

      // 应用于哪些路由,path需要准确,但是不需要/halfMonthApi
      .forRoutes({ path: '/user/list', method: RequestMethod.GET });
  }
}

注意: 目前测试的结果是 exclude 时候path要加上app.setGlobalPrefix('/halfMonthApi') 的前缀"/halfMonthApi",

但forRoutes时却不需要加前缀

Nestjs的拦截器

每个拦截器都实现了 intercept() 方法,它有两个参数。第一个是 ExecutionContext 实例(与 guards 完全相同的对象)。第二个参数是 CallHandlerCallHandler 接口实现了 handle() 方法,你可以使用它在拦截器中的某个点调用路由处理程序方法。如果在 intercept() 方法的实现中不调用 handle() 方法,则根本不会执行路由处理程序方法。

  • 使用 @Injectable 装饰,必须实现 NestInterceptor 接口
  • 在函数执行前后绑定额外的逻辑
  • 转换一个函数返回值
  • 转换函数抛出的异常
  • 扩展基础函数行为
  • 根据特定条件完全重写一个函数
typescript
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } 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`)),
      )
  }
}

// 通过 @UseInterceptors(LoggingInterceptor) 使用
typescript
// 将所有响应中出现的 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 ))
  }
}

尝试写一个拦截器

第一步: 新建 src/common/exceptions/base.exception.filter.tshttp.exception.filter.ts 两个文件,从命名中可以看出它们分别处理统一异常与 HTTP 类型的接口相关异常。

base.exception.filter => Catch 的参数为空时,默认捕获所有异常

typescript
import { FastifyReply, FastifyRequest } from "fastify";

import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpStatus,
    ServiceUnavailableException,
    HttpException,
} from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
    catch(exception: Error, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<FastifyReply>();
        const request = ctx.getRequest<FastifyRequest>();

        request.log.error(exception)
        
        // 非 HTTP 标准异常的处理。
        response.status(HttpStatus.SERVICE_UNAVAILABLE).send({
            statusCode: HttpStatus.SERVICE_UNAVAILABLE,
            timestamp: new Date().toISOString(),
            path: request.url,
            message: new ServiceUnavailableException().getResponse(),
        });
    }
}

http.exception.filter.ts => Catch 的参数为 HttpException 时, 将只捕获 HTTP 相关的异常错误

typescript
import { FastifyReply, FastifyRequest } from "fastify";
import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<FastifyReply>();
        const request = ctx.getRequest<FastifyRequest>();
        const status = exception.getStatus();

        response.status(status).send({
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
            message: exception.getResponse(),
        });
    }
}

第二步: 在 main.ts 文件中添加 useGlobalFilters 全局过滤器:

typescript
+ import { AllExceptionsFilter } from './common/exceptions/base.exception.filter';
+ import { HttpExceptionFilter } from './common/exceptions/http.exception.filter';
  // 异常过滤器
+ app.useGlobalFilters(new AllExceptionsFilter(), new HttpExceptionFilter());

除了全局异常拦截处理之外,我们需要再新建一个 business.exception.ts 来处理业务运行中预知且主动抛出的异常:

typescript
import { HttpException, HttpStatus } from '@nestjs/common';
import { BUSINESS_ERROR_CODE } from './business.error.codes';

type BusinessError = {
    code: number;
    message: string;
};

export class BusinessException extends HttpException {
    constructor(err: BusinessError | string) {
        if (typeof err === 'string') {
            err = {
                code: BUSINESS_ERROR_CODE.COMMON,
                message: err,
            };
        }
        super(err, HttpStatus.OK);
    }
    
    static throwForbidden() {
        throw new BusinessException({
            code: BUSINESS_ERROR_CODE.ACCESS_FORBIDDEN,
            message: '抱歉哦,您无此权限!',
        });
    }
}
typescript
export const BUSINESS_ERROR_CODE = {
    // 公共错误码
    COMMON: 10001,
    // 特殊错误码
    TOKEN_INVALID: 10002,
    // 禁止访问
    ACCESS_FORBIDDEN: 10003,
    // 权限已禁用
    PERMISSION_DISABLED: 10003,
    // 用户已冻结
    USER_DISABLED: 10004,
};

最后改造一下 HttpExceptionFilter,在处理 HTTP 异常返回之前先处理业务异常:

typescript
import { FastifyReply, FastifyRequest } from "fastify";
import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpException,
    HttpStatus,
} from '@nestjs/common';
import { BusinessException } from "./business.exception";

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<FastifyReply>();
        const request = ctx.getRequest<FastifyRequest>();
        const status = exception.getStatus();

        // 处理业务异常
        if (exception instanceof BusinessException) {
            const error = exception.getResponse();
            response.status(HttpStatus.OK).send({
                data: null,
                status: error['code'],
                extra: {},
                message: error['message'],
                success: false,
            });
            return;
        }

        response.status(status).send({
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
            message: exception.getResponse(),
        });
    }
}

拦截器的使用

拦截器可以通过装饰器应用到控制器或特定的方法上。

typescript
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './interceptors/logging.interceptor';

@Controller('cats')
@UseInterceptors(LoggingInterceptor) // 应用于整个控制器
export class CatsController {
  @Get(':id')
  @UseInterceptors(LoggingInterceptor) // 应用于特定方法
  findOne() {
    return 'Details of cat';
  }
}

还可以在全局范围内应用拦截器,这样就不需要在每个控制器方法中单独定义。

typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { PerformanceInterceptor } from './interceptors/performance.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new PerformanceInterceptor());
  await app.listen(3000);
}
bootstrap();

NestJS 提供了一些内置的拦截器:

  • ClassSerializerInterceptor: 用于序列化和反序列化对象,确保响应对象只包含预期的属性。
  • TransformInterceptor:用于转换传入的数据,可以用于自定义数据转换逻辑。
  • SerializeInterceptor:用于序列化响应数据,可以用于确保响应数据符合一定的格式。
  • DeserializeInterceptor:用于反序列化请求数据,可以用于确保请求数据符合一定的格式。

Nestjs的管道

  • 控制器接收请求 前 处理,偏向于服务端
  • interceptor start -> pipe validate -> handler -> interceptor end
  • 使用 @Injectable 装饰,必须实现 PipeTransform 接口
  • 转换输入数据为目标格式 && 验证输入数据
  • 管道会在异常范围内执行 - 异常处理层可以处理管道异常
  • 内置 ValidationPipeParseIntPipe
typescript
// 配合第三方库使用
// yarn add class-validator class-transformer
import { validate } from 'class-validator'
import { plainToClass } 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 = plainToClass(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)
  }
}

// 通过 @UsePipes(ValidationPipe) 使用

绑定管道

要使用管道,我们需要将管道类的实例绑定到适当的上下文。如果我们希望将管道与特定的路由处理程序方法相关联,并确保它在调用该方法之前运行。我们称为在方法参数级别绑定管道:

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

定制管道

每个管道都必须实现 transform() 方法来履行 PipeTransform 接口契约。这个方法有两个参数:

value 参数是当前处理的方法参数(在被路由处理方法接收之前),metadata 是当前处理的方法参数的元数据。元数据对象具有以下属性:

typescript
export interface ArgumentMetadata {
    type: 'body' | 'query' | 'param' | 'custom';
    metatype?: Type<unknown>;
    data?: string;
}

全局作用域管道

使用 useGlobalPipes 实现,使其应用于整个应用中的每个路由处理程序。

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

NestJS 提供了一些内置的管道,可以开箱即用:

  • ValidationPipe:可以确保传入的数据符合一定的验证规则,并且可以将数据转换成期望的格式。
  • ParseIntPipe:将数据转换为整数。
  • ParseFloatPipe:将数据转换为浮点数。
  • ParseBoolPipe:将数据转换为布尔值。
  • ParseArrayPipe:将数据转换为数组。
  • ParseEnumPipe:将数据转换为枚举。
  • ParseUUIDPipe:将数据转换为UUID。
  • DefaultValuePipe:用于在传入的参数为空或未定义时提供一个默认值。

异常过滤器

异常过滤器是负责处理应用中所有未处理的异常。当你的应用代码未处理异常时,该层会捕获该异常,然后自动发送适当的用户友好响应。

  • 默认情况下会被 全局 异常过滤器 HttpException 或它的子类处理
  • 若未识别,会返回给客户端 500, Internet server error 的报错

抛出标准异常

Nest 提供了一个内置的 HttpException 类,从 @nestjs/common 包中暴露出来。假如如下代码在这里因某种原因抛出异常。

typescript
@Get()
async findAll() {
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

在客户端调用此接口的时候,响应如下展示:

json
{
    "statusCode": 403,
    "message": "Forbidden"
}

自定义异常

ts
// @Catch 装饰, 并实现 ExceptionFilter 接口
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'
import { Request, Response } from 'express'

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()
    const status = exception.getStatus()

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      })
  }
}

// 通过 @UseFilters(HttpExceptionFilter) 使用
// 方法作用域、控制器作用域、全局作用域...

自定义的异常过滤器参考1

要创建一个异常过滤器,需要定义一个类,并实现 ExceptionFilter 接口。这个接口定义了一个 catch 方法,该方法接收异常实例、上下文和调用处理器作为参数。

typescript
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch() // 标记为全局异常过滤器
export class AllExceptionsFilter implements ExceptionFilter {
  // 方法用于处理异常,并返回一个标准的 JSON 格式的错误响应。
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception instanceof HttpException ? exception.message : 'Internal server error',
    });
  }
}

异常过滤器可以通过装饰器应用到控制器或特定的方法上,也可以全局应用。

ts
import { Controller, Get, UseFilters } from '@nestjs/common';
import { AllExceptionsFilter } from './filters/all-exceptions.filter';

@Controller('cats')
@UseFilters(AllExceptionsFilter) // 应用于整个控制器
export class CatsController {
  @Get()
  findAll() {
    // 抛出异常
    throw new Error('An error occurred');
  }
}

全局应用异常过滤器

ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './filters/all-exceptions.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // 全局应用异常过滤器
  app.useGlobalFilters(new AllExceptionsFilter());
  await app.listen(3000);
}
bootstrap();

Nestjs的请求参数

路由参数

当我们需要接收动态参数作为请求的一部分时,我们可以在路由中添加路由参数标记,以捕获请求 URL 中该位置的动态值。

typescript
    @Get(':id')
    getUser(@Param() params): string {
        return `User: my name is nest user${params.id}`;
    }

我们同样可以将特定的参数标记传递给@Param()装饰器,然后在方法主体中按名称直接引用路由参数。

typescript
   @Get(":id")
    getUser(@Param("id") id): string {
        return `User: my name is nest user${id}`;
    }

使用Post请求接收任何客户端参数

我们可以通过@Body()装饰器来完成这个工作。 不过首先我们需要定义一个类,比如User:

ts
export class CreateUserDao {

    // 用户姓名
    readonly name: string;
    // 用户年龄
    readonly age: number;
    // 用户性别
    readonly gender: number;
}

接下来我们在控制器中使用我们定义好的数据类:

typescript
@Post("createuser")
createUser(@Body() createUserDao: CreateUserDao): string {
    return `this is a new user: ${createUserDao.name}`
}

如果只想接收一部分,可以向@Body()中传入key

ts
@Post('nice')
findTwo(@Body('name') body: any) {
  return body;
}

动态路由匹配 @Param

typescript
import { Controller, Get, Param } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('list/:id')
  getHello(@Param() params): string {
    const { id } = params;
    return id;
  }
}

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

typescript
//...

@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('list/:id')
  getHello(@Param('id') id): string {
    return id;
  }
}

query接参 @Query

typescript
import { Controller, Get, Query } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@Query() params): string {
    const { id } = params;
    return id;
  }
}

特别注意

提示:@Body()装饰器,只能解析JSON格式或x-www-form-urlencoded格式。Form Data格式一般用于上传文件,需使用FileInterceptor。对于专用于数据上报的navigator.sendBeacon,传输的是text/plain格式,需自定义参数装饰器来解析

新建plain-body.decorator.ts

typescript
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import * as rawbody from 'raw-body';

export const PlainBody = createParamDecorator(
  async (data: unknown, ctx: ExecutionContext) => {
    const req = ctx.switchToHttp().getRequest();
    if (req.readable) {
      const data = (await rawbody(req)).toString().trim();
      try {
        return JSON.parse(data);
      } catch {
        return data;
      }
    } else {
      return data;
    }
  },
);

使用:

typescript
@Post('/track')
async track(@PlainBody() params) {
  //...
}

获取请求头 @Headers

除了上述常见的三种传参方式,在请求头中获取参数也是常见的业务需求。

import { Controller, Headers, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('app')
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@Headers() headers): string {
    const { userid } = headers;
    return userid;
  }
}

使用Postman访问GET请求localhost:3000/api/app,设置请求头{userid:123},返回123。

提示:请求头headers中的参数都是短横线命名法,即使用userid或user-id,不要使用userID。如果请求的时候,请求头中使用了userID,那么在Nest路由接参时,还是取的是userid。

@Headers() 是参数装饰器,用于在一个请求处理中获取需要的请求头信息。

@Header() 是方法装饰器,用于在一个请求中设置响应头信息。

js
import { Controller, Post, Headers, Header } from '@nestjs/common';
import { UserService } from './user.service';


@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  // 设置响应头
  @Header('user', 'kw')
  // 获取请求头
  create(@Headers('Content-Type') contentType: string) {
    console.log(contentType);
    return 'nest controller'
  }
}

在 Postman 中发送一个 Post 请求,传递的是 JSON 格式的数据,则 Postman 自动处理 Content-Typeapplication/json

验证传入的请求(ValidationPipe)

首先全局引入这个对象:

typescript
import { ValidationPipe } from '@nestjs/common';

// main.ts中
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());  // 这里
  await app.listen(3000);
}
bootstrap();

然后安装:

shell
pnpm i class-validator class-transformer

DTO中添加规则

typescript
import { IsString } from 'class-validator';

export class CreateCoffeeDto {
  @IsString()
  readonly name: string;

  @IsString()
  readonly brand: string;

  @IsString({each: true})  // each:true代表期望的是一个字符串数组
  readonly flavors: string[];
}
// 它还有很多其他有用的规则,请查看文档

此时,如果我们请求的数据不符合我们的规则,就会返回400

json
{
    "name":"Shipwreak Roast"
}
// 返回
{
    "statusCode": 400,
    "message": [
        "brand must be a string",
        "each value in flavors must be a string"
    ],
    "error": "Bad Request"
}

Pipe管道与DTO

在main.ts中启用管道

app.useGlobalPipes(new ValidationPipe());

下载两个包

yarn add class-validator class-transformer

其中:class-validator可以让你使用DTO时,如果传值类型错误,会给你提供详细的错误信息

在create-coffee.dto.ts中使用:

import { IsString } from 'class-validator';

export class CreateCoffeeDto {
  @IsString()
  readonly id: string;

  @IsString()
  readonly name: string;

  @IsString()
  readonly brand: string;

  @IsString({ each: true })
  readonly flavors: string[];
}

在postman中测试:

// 输入
{
    "name": "year",
    "brand": "nest-ts"
}
//输出
{
    "statusCode": 400,
    "message": [
        "id must be a string",
        "each value in flavors must be a string"
    ],
    "error": "Bad Request"
}

这样,我们得到了详细的错误提示:id必须是string类型,flavor必须是string[]类型

我们重新修改过后:

// 成功录入
{
    "id": "2",
    "name": "nest",
    "brand": "coffee",
    "flavors": [
        "nice",
        "vanlilla"
    ]
}

为了不在update-coffee.dto.ts中也写同样的代码。nestjs给我们提供了一个包:mapped-types

yarn add @nestjs/mapped-types

我们让类继承PartialTyp(),参数为想要继承属性的class。继承的属性全部都为可选,类型也都是相同的。

update-coffee.dto.ts:

import { PartialType } from '@nestjs/mapped-types';
import { CreateCoffeeDto } from './create-coffee.dto';

export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {}

coffees.service.ts:

// 更新指定id的coffee
update(id: string, updateDto: UpdateCoffeeDto) {
  let existingCoffee = this.findOne(id);
  const coffeeIndex = this.coffees.findIndex((item) => item.id === id);
  if (!existingCoffee) {
    throw new HttpException(`#${id}# is not found`, 401);
  }
  Object.assign(this.coffees[coffeeIndex], updateDto);
}

测试:

// 输入
{
    "name": 132,
    "brand": "coffee"
}
// 输出
{
    "statusCode": 400,
    "message": [
        "name must be a string"
    ],
    "error": "Bad Request"
}

当我们将name的值设为string类型时(如"name" : "ddwb"),就不会报错了。然后再查询:

{
    "id": "1",
    "name": "ddwb",
    "brand": "coffee",
    "flavors": [
        "sweet",
        "vanlilla"
    ]
}

class-validator 校验器的使用