NestJs 基础概念

1,715 阅读26分钟

概念

创建项目

npm i -g @nestjs/cli


nest new project-name

默认创建了如下的目录

image.png

目录代表意义

app.controller.ts带有单个路由的基本控制器。
app.controller.spec.ts针对控制器的单元测试。
app.module.tsT应用程序的根模块(root module)。
app.service.ts具有单一方法的基本服务(service)。 method.
main.ts应用程序的入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序的实例。

启动项目

npm run start

main.ts 作为入口文件,包含了一个异步函数,用来引导项目的启动过程,初始化的项目的内容如下:

import { NestFactory } from '@nestjs/core';


import { AppModule } from './app.module';


async function bootstrap() {


  const app = await NestFactory.create(AppModule);


  await app.listen(3000);


}


bootstrap();

NestFactory @nestjs/core的核心类,暴露了一些静态方法用于创建应用程序的实例。方法返回一个应用程序的对象,实现了INestApplication接口。

Controllers

概念:负责处理发过来的请求和给客户端返回请求,路由控制器控制哪个 Controller 处理哪个请求,通常一个控制器有多个路由,不同的路由可以执行不同的动作。

image.png

Routing

我们使用 @Controller() 装饰器来定义一个基础的controller,在 @Controller()里面添加一个路由前缀让我们更加轻松的划分路由分组,尽可能减少代码体积。

@@filename(cats.controller)

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

@@switch
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}

@Get() 装饰器在 findAll 的前面,告诉nestHTTP请求创建一个图书的端点,这个端点相当于HTTP请求的的路径和路由路径(处理程序的路由路径是通过连接为控制器声明的(可选的)前缀和在请求装饰器中指定的任何路径来确定的)。Nest将把GET /cats请求映射到这个处理程序,所以我们的路由路径是一个组合的结果。eg:

@@filename(cats.controller)

import { Controller, Get } from '@nestjs/common';

@Controller('customers')

export class CatsController {

  @Get('profile')

  findAll(): string {

    return 'This action returns all /customers/profile';

  }

}

Request object

Nest提供对底层平台的请求对象的访问,我们可以添加@Req()装饰器来指示Nest注入请求对象来访问请求对象。

@@filename(cats.controller)


import { Controller, Get, Req } from '@nestjs/common';


import { Request } from 'express';

@Controller('cats')


export class CatsController {


  @Get()


  findAll(@Req() request: Request): string {


    return 'This action returns all cats';

  }

}

Request object:代表的是HTTP的请求,拥有很多http请求的属性如:query string 、HTTP headers,HTTP body,在大部分的情况下这些属性在主观上是没有意义的,我们可以使用装饰器来精简这些属性:

名称代表值
@Request(), @Req()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
@HostParam()req.hosts

Resources

我们使用GET 来获取所有的数据,现在我们也可以通过POST 来创建一条数据。

@@filename(cats.controller)


import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')


export class CatsController {


  @Post()


  create(): string {


    return 'This action adds a new cat';


  }
  @Get()
  findAll(): string {

    return 'This action returns all cats';

  }

}

Nest 请求装饰器

@Get()、@Post()、@Put()、@Delete()、@Patch()、@Options()、@Head()、@All()

HTTP 没有 All 方法,这是一个快捷方法用来接收任何类型的 HTTP 请求。

Route wildcards

对路由的匹配也支持通过通配符来进行匹配

@Get('ab*cd')


findAll() {


  return 'This route uses a wildcard';


}

上面的 'ab*cd' 可以匹配所有的上诉情况 abcd, ab_cd, abecd,详细情况于正则表达式相匹配

Status code

我们可以通过在处理程序级别添加@HttpCode(…)装饰器轻松地改变状态码。

@Post()


@HttpCode(204)


create() {


  return 'This action adds a new cat';


}

Headers

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

@Post()


@Header('Cache-Control', 'none')


create() {


  return 'This action adds a new cat';


}

Redirection

想要对特殊的路有精心重定向,我们可以使用 @Redirect()@Redirect()接受一个必需的url参数和一个可选的statusCode参数。如果省略,statusCode默认为302 (Found)。

@Get()

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

返回的值将覆盖传递给@Redirect()装饰器的任何参数

@Get('docs')

@Redirect('https://docs.nestjs.com', 302)

getDocs(@Query('version') version) {

  if (version && version === '5') {

    return { url: 'https://docs.nestjs.com/v5/' };

  }

}

Route parameters

为了定义路由的参数,我们可以给参数添加token去在url中标识动态数据,以这种方式声明的路由参数可以使用@Param()装饰器访问,该装饰器应该添加到方法签名中。

@@filename()
@Get(':id')

findOne(@Param() params): string {

console.log(params.id);

return `This action returns a #${params.id} cat`;

}

我们可以通过引用params.id来访问id参数。您还可以将特定的参数token传递给装饰器,然后通过方法体中的名称直接引用路由参数。

@@filename()
@Get(':id')

findOne(@Param('id') id: string): string {

    return `This action returns a #${id} cat`;

}

Sub-Domain Routing

@Controller装饰器可以采用一个host选项,要求传入请求的HTTP host匹配某些特定的值。

@controller({host:"admin.example.com})

export class AdminController{
    @Get()
    index():string{
        return 'admin page'
    }
}

Scopes

在Nest中,几乎所有的东西都是在传入请求之间共享的。存在一个数据库的连接池,带有全局状态的单例服务.

Asynchronicity

JavaScript 数据的提取一般都是异步的,nestjs 是基于JavaScript的,所以,nest对于 async 函数支持的很好。 每一个async 函数必须要返回一个Promise,这意味着你可以返回一个Nest能够自行解析的延迟值。

@controller(cats.controller)

export class AdminController{
  @Get()
    async findAll(): Promise<any[]> {
        return [];
    }
}

payloads

我们之前的例子中POST没有处理来自客户端的请求参数,可以使用 @Body 装饰器。 我们使用 TypeScript,我们需要先进行定义DTO(数据传输对象),该对象定义了在网络中数据传输的格式,可以使用 class 和 interface 的方式进行定义,但是更加倾向于使用class进行定义,因为class 符合es6的标准,使用其他的可能存在额外的性能问题。

@@filename(create-cat.dto)
export class CreateCatDto {

    name: string;

    age: number;

    breed: string;
}

我们可以在controller 中使用DTO

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

Full resource sample

下面是一个使用几种可用装饰器来创建基本控制器的例子。这个控制器公开了几个方法来访问和操作内部数据。

@@filename(cats.controller)
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`;
}
}

Getting up and running

虽然CatsController已经完全定义好了,但是Nest还是不知道这个 controller 的存在,也没有办法创建一个类的实例。controller 在项目中属于module,我们引入一个 controllers 的数组在 @Modules装饰器中,在例子中我们还没有定义其他的模块,所以我们现在可以在 AppModule中引入 `CatsController

@@filename(app.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

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

Library-specific approach

之前我们讨论了使用 Nest 标准去操作响应数据,还有第二种方法去 操作响应数据就是 Library-specific approach,为了注入一个特定的请求对象,我们需要使用 @Res装饰器,为了展示不同我们按照如下方式重写了CatsController

@@filename()
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([]);
  }
}

上面的这种方式可以使用,但是使用的过程需要慎重,因为这种方法不太清晰,而且也的确有很多的问题。最主要的问题:代码会变得依赖平台而且不易于测试。此外,在上面的示例中,您将失去与依赖于Nest标准响应处理的Nest特性的兼容性,比如拦截器和@HttpCode() / @Header()装饰器。为了解决这个问题我们可以设置 passthroughtrue

@@filename()
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

Porviders

概念:Providers 在Nest中是一个很基本的概念,许多基本的Nest类都可以被视为服务提供者、存储库、工厂、助手等等。Providers的主旨就是注入以来。这意味着对象之间可以创建各种关系,“连接”对象实例的功能可以在很大程度上委托给Nest运行时系统。 image.png

之前,我们构建了一个简单的CatsControllerController应该处理HTTP请求,并将更复杂的任务委托给ProvidersProviders是在模块中声明为Providers的普通JavaScript类。

Services

让我们从创建一个简单的 CatsService来开始学习,该服务将负责数据存储和检索。而且他设计用来服务于CatsController,因此,可以将其定义为Providers

@@filename(interfaces/cat.interface)
export interface Cat {
  name: string;
  age: number;
  breed: string;
}
@@filename(cats.service)
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;
  }
}

例子中的CatsService 是一个很基础的类,只有一个属性和两个方法。这里比较新的用法就是使用了@Injectable()装饰器,@Injectable()装饰器附加元数据,它声明CatsService是一个可以由Nest IoC容器管理的类。 下面我们在Controllers中使用一下这个Service

@@filename(cats.controller)
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);
 }

 @Get()
 async findAll(): Promise<Cat[]> {
   return this.catsService.findAll();
 }
}

CatsService 通过类进行注册,注意私有语法的使用

Dependency injection

Nest是围绕通常被称为依赖注入的强大设计模式构建的。由于TypeScript的能力,管理依赖非常容易,因为只需要根据类型解析,Nest将通过创建并返回一个catsService实例来解析该catsService。

constructor(private catsService: CatsService) {}

Scope

Providers一般和程序的生命周期是一致的,当应用程序启动时,必须解析每个依赖项,因此必须实例化每个Providers。相似的,当程序被注销的时候,每一个Providers也会被注销。但是,也有一些方法可以使Providers的生命周期限定在请求范围内。

Custom providers

Nest有一个内置的控制反转(“IoC”)容器,用来解决Providers之间的关系,这个特性是我们上面描述的依赖注入的基础,但是它的能力远比我们描述的强大。

Optional providers

有些时候,一些依赖我们没有必要一定去解析。比如说:你的类依赖一个配置对象。但是如果没有传递,则应该使用默认值,在这种情况下依赖变成了可选项,因为缺少这样一份依赖并不会导致什么错误。为了表明这个依赖是可选的,我们使用@Optional()来表明这个构造器是可选的。前面的例子显示了基于构造函数的注入,通过构造函数中的类指示依赖关系。

import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

Property-based injection

迄今为止我们所用的技术是基于构造器的注入,因为 providers的注入方式是通过 constructor的方式进行注入的,但是在很多的特殊案例中,property-based injection 的方法是更加有效的。比如说:如果你的顶级类依赖于一个或多个providers,通过在子类中从构造函数调用super()来传递它们是非常繁琐的,为了避免这个情况我们使用@Inject()装饰器在属性层级。

import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}

Provider registration

现在我们已经定一个了一个ProviderCatsService,并且我们也有一个自定义的service CatsController ,我们需要在Nest中注册这个service,这样它就可以进行注入,现在我们可以修改我们的module 文件,将服务添加到 @Module()装饰器的providers数组中。

@@filename(app.module)
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 {}

当面的目录应该是:

image.png

Modules

概念:模块是用@Module()装饰器注解的类。@Module()装饰器提供了Nest用来组织应用结构的元数据。

image.png

每一个应用至少要有一个 module(根模块),根模块是用来构建nest程序图的起点-- Nest用来解析moduleprovider关系和依赖关系的内部数据结构。但是很少有程序真的就只拥有一个根模块,我们要推荐使用模块作为组织组件的有效方式。因此,对于大多数应用程序,最终的体系结构将采用多个模块,每个模块封装了一组密切相关的功能。

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

功能描述
providers这些提供商将被Nest注入器实例化,并且至少可以在这个模块中共享
controllers这个模块中定义的一组必须被实例化的控制器
imports导出此模块中所需的提供程序的导入模块列表
exports由该模块提供的providers子集,应该在导入该模块的其他模块中可用

module 默认封装了 providers。这意味着不可能注入既不是当前模块的直接组成部分,也不可能从导入的模块导出的提供商。因此,我们可以将从模块导出的提供程序视为该模块的公共接口或API。

Feature modules

CatsController 和 CatsService属于同一个应用空间。因为他们是紧密关联的,所以现在有一个场景将他们划分到同功能模块,功能模块只是简单地组织与功能特性相关的代码,保持代码的组织并建立清晰的边界。这有助于我们管理复杂性并使用SOLID原则进行开发,特别是当应用程序和/或团队的规模增长时。

为了演示这个功能,我们在下面创建一个这样的module

@@filename(cats/cats.module)
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命令即可。

上面,我们在cats.module.ts文件中定义了CatsModule,并将所有与该模块相关的内容移到cats目录中。我们需要做的最后一件事是将这个模块导入到根模块中。

@@filename(app.module)
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';

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

当前的文件目录:

image.png

Shared modules

在Nest中,模块默认是单例的,因此你可以在多个模块之间轻松地共享任何providers的同一个实例。

image.png

每个模块都自动成为一个共享模块, 一旦创建,它可以被任何模块重用。让我们设想一下,我们想要在几个模块之间分享 CatsService的实例,为了做这件事情我们需要先导出CatsService通过添加exports数组。

@@filename(cats.module)
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,并将与所有导入它的模块共享相同的实例。

Module re-exporting

如上所示,模块可以导出它们的内部提供程序。此外,他们可以重新导出他们导入的模块。

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

Dependency injection

模块类也可以注入Provider。

@@filename(cats.module)
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 catsService: CatsService) {}
}

Global modules

如果你想在很多的module 都引入一写相同的module,这些按照之前的写法会比较繁琐。与Nest不同,Angular的Providers是在全局作用域中注册的。一旦注册了,在任何地方均可以使用。然而,Nest将Providers封装在模块范围内。如果不先导入封装的模块,你就不能在其他地方使用模块的Providers

当你想要任何地方都可以使用一些模块的时候,你可以使用@Global()装饰器。

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()装饰器让 模块拥有全局作用域的能力,全局模块应该只注册一次,通常由根模块或核心模块注册

所有东西都全球化并不是一个好的设计决策。全局模块可用来减少必要的样板的数量。导入数组通常是使模块的API对消费者可用的首选方式。

Dynamic modules

Nest模块系统包含一个强大的特性,称为动态模块,这个特性使您能够轻松地创建自定义的模块,可以动态地注册和配置`Providers`

下面是一个DatabaseModule动态模块定义的例子:

@@filename()
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,
    };
  }
}

默认情况下,该模块定义了Connectionprovider,但是除此之外,依赖于使用entitiesoptions对象向forRoot方法传入数据,公开一些provider的集合,比如说 仓库。动态模块返回的属性扩展(而不是覆盖)了@Module()装饰器中定义的基本模块元数据。

如果你想在全局范围内注册一个动态模块,将global属性设置为true

{
    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 {}

如果你想反过来重新导出一个动态模块,你可以省略exports数组中的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 {}

Middleware

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

image.png

默认情况下,Nest中间件等同于表示中间件,下面来自官方文档的描述描述了中间件的功能:

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

你可以在函数或带有@Injectable()装饰器的类中实现自定义的Nest中间件。但这个函数没有什么特殊的需求的时候,这个类应该实现NestMiddleware接口。我们可以通过下面的例子实现一个简单的通过类的方法实现的方法。

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

@Injectable()
export class LoggerMiddleware {
  use(req, res, next) {
    console.log('Request...');
    next();
  }
}

Dependency injection

Nest中间件完全支持依赖注入。就像providerscontrollers一样,它们能够注入同一模块中可用的依赖项。通常,这是通过构造函数完成的。

Applying middleware

@Module()装饰器中没有中间件的位置。相反,我们使用模块类的configure()方法来设置它们。包含中间件的模块必须实现NestModule接口。让我们将LoggerMiddleware设置在AppModule级别。

@@filename(app.module.ts)
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');
  }

在上面的例子中,我们已经为/cats路由处理程序设置了LoggerMiddleware,这个中间件是之前在CatsController中定义的。在配置中间件时,我们还可以将包含路由路径和请求方法的对象传递给forRoutes()方法,从而进一步将中间件限制为特定的请求方法,在下面的例子中,注意我们导入了RequestMethod枚举来引用所需的请求方法类型。

@@filename(app.module.ts)
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 });
  }
}

ps:可以使用async/awaitconfigure()方法变成异步的。

Route wildcards

也支持基于模式的路由。例如,星号用作通配符,将匹配任何字符的组合:

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

Middleware consumer

MiddlewareConsumer 是一个辅助类,它提供几种构建方法去管理中间件,所有这些可以简单地串联在流利的风格,forRoutes()这个方法的阐述可以是一个简单的字符串、多个字符串,一个 RouteInfo对象,一个控制器类型、甚至是多个控制器类。在很多的案例中,我们可能只是传递一个用逗号分隔的控制器的list,现在我们在下面的例子中传入的是一个控制器类。

@@filename(app.module.ts)
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';

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes(CatsController);
  }
}

Excluding routes

有时,我们想要排除某些路由应用中间件,我们可以很容易地使用exclude()方法排除某些路由。可以使用一个字符串、多个字符串或一个RouteInfo对象来标识要排除的路由。

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats', method: RequestMethod.GET },
    { path: 'cats', method: RequestMethod.POST },
    'cats/(.*)',
  )
  .forRoutes(CatsController);

LoggerMiddleware将被绑定到所有在catscontroller中定义的路由,除了三个传递给exclude()方法的路由。

Functional middleware

我们写的LoggerMiddleware类十分简单。他没有成员、没有额外的方法,没有依赖。那为什么我们要定义这个简单的方法。我们叫这样的中间件functional middleware,我们将这个登录的中间件从以类为基础的转换成函数为基础的,来说明两者的区别:

@@filename(logger.middleware.ts)
import { Request, Response, NextFunction } from 'express';

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

在AppModule中如何使用呢:

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

Multiple middleware

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

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

Global middleware

当我们想要一次性绑定中间件在每一个注册路由,我们可以使用由INestApplication实例提供的 ,use()方法。

Exception filters

概念:Nest自带一个内置的异常层,负责处理整个应用程序中所有未处理的异常,当在程序代码中没有处理这个异常,他就会被这一层catch到。然后自动发送适当的用户友好的响应。

image.png 开箱即用,此操作由内置全局异常过滤器执行,该过滤器处理HttpException类型的异常(及其子类),当一个异常无法识别时(既不是HttpException,也不是从HttpException继承的类),内置的异常过滤器会生成以下默认JSON响应:

{
  "statusCode": 500,
  "message": "Internal server error"
}

Throwing standard exceptions

Nest提供了一个内置的HttpException类,从@nestjs/common包中公开,对于典型的基于HTTPREST/GraphQL API的应用程序,最佳实践是在出现某些错误条件时发送标准HTTP响应对象。

例如,在CatsController中,我们有一个findAll()方法(GET路由处理程序)。让我们假设这个路由处理程序出于某种原因抛出了一个异常。为了演示这一点,我们将其硬编码如下:

@@filename(cats.controller.ts)
@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

当客户端调用这个端点时,响应看起来像这样:

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

HttpException构造函数有两个必需的参数来决定响应:

  • response参数定义JSON响应体。它可以是字符串或如下所述的对象。
  • status 参数定义的是 HTTP的状态码 默认状态下,JSON 返回值的body 包括下面两个属性
  • statusCode:默认为status参数中提供的HTTP状态码
  • message:基于状态的HTTP错误的简短描述

要覆盖JSON响应体的消息部分,请在响应参数中提供一个字符串。要覆盖整个JSON响应体,请在响应参数中传递一个对象。Nest将序列化该对象并将其作为JSON响应体返回。

第二个构造函数参数status应该是一个有效的HTTP状态码。最佳实践是使用从@nestjs/common导入的HttpStatus枚举。

@@filename(cats.controller.ts)
@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
}

使用上面的方法,下面是响应的样子:

{
  "status": 403,
  "error": "This is a custom message"
}

Custom exceptions

在很多情况下,我们都需要写一个自定义的异常,可以使用内置的Nest HTTP异常,和下一个模块描述的一样,如果你想要创建一个自定义的异常,创建自己的异常层次结构是一种很好的实践,你的自定义异常继承于HttpException基类,使用这种方法,Nest将识别出你的异常,并自动处理错误响应。让我们实现这样一个自定义异常。

@@filename(forbidden.exception)
export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

由于ForbiddenException扩展了基础HttpException,它将与内置的异常处理程序无缝地工作,因此我们可以在findAll()方法中使用它。

@@filename(cats.controller)
@Get()
async findAll() {
  throw new ForbiddenException();
}

Built-in HTTP exceptions

Nest提供了一组从基本HttpException继承的标准异常。这些是从@nestjs/common包中公开的,代表了许多最常见的HTTP异常:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

Exception filters

虽然基本(内置)异常过滤器可以自动为我们处理许多情况,但可能还是希望完全控制异常层,例如,您可能希望根据一些动态因素添加日志记录或使用不同的JSON schemaException filters正是为此目的而设计的。它们允许我们控制确切的控制流和发送回客户机的响应的内容。

让我们创建一个异常过滤器,它负责捕获HttpException类的一个实例中的异常,并为它们实现自定义的响应逻辑。为此,我们需要访问底层平台RequestResponse对象。我们可以介入 Request对象方便我们从原始url中获取登陆信息。我们将使用Response对象直接控制发送的响应,使用Response.json()方法。

@@filename(http-exception.filter)
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,
      });
  }
}

所有异常过滤器应该实现通用的ExceptionFilter<T>接口。这需要您提供带有指定签名的catch(exception: T, host: ArgumentsHost)方法。T表示异常类型。

 @Catch(HttpException) 装饰器将所需的元数据绑定到异常过滤器,告诉Nest这个特定的过滤器正在寻找HttpException类型的异常,而不是其他类型的异常。@Catch()装饰器可以只接受一个参数,或者一个逗号分隔的列表。允许一次为几种类型的异常设置过滤器。

Arguments host

让我们看一下 catch() 的参数列表,异常参数是当前正在处理的异常对象。host参数是ArgumentsHost类型对象。在下面的例子中,我们使用他来指向原始requestRequestResponse对象。我们在ArgumentsHost上使用了一些辅助方法来获得所需的RequestResponse对象。

Binding filters

让我们将新的HttpExceptionFilter绑定到CatsControllercreate()方法。

@@filename(cats.controller)
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

@UseFilters()装饰器是从@nestjs/common包中导入的。

我们在这里使用了 @UseFilters装饰器,与@Catch()装饰器相似,它可以使用单个筛选器实例,也可以使用逗号分隔的筛选器实例列表。在例子中我们创建了一个 HttpExceptionFilter实例。或者,我们可以传递类(而不是实例),将实例化的工作留给框架,并启用依赖注入。


@@filename(cats.controller)
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

如果可能的话,最好使用类而不是实例来应用过滤器。它减少了内存的使用,因为Nest可以轻松地在整个模块中重用同一个类的实例。

在上面的示例中,HttpExceptionFilter只应用于单个create()路由处理程序,使其限于方法范围。异常筛选器可以定义在不同的级别:方法作用域、控制器作用域或全局作用域。例如,要将过滤器设置为控制器作用域,你需要执行以下操作:


@@filename(cats.controller)
@UseFilters(new HttpExceptionFilter())
export class CatsController {}

这个构造为在CatsController中定义的每个路由处理程序设置HttpExceptionFilter

我们创建一个全局作用域:

@@filename(main)
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
bootstrap();

全局范围的过滤器在整个应用程序中使用,用于每个控制器和每个路由处理程序。在依赖注入方面,全局过滤器从任何模块外部注册,例子中的不能注入依赖,因为这是在任何模块的上下文之外完成的。了解决这个问题,你可以使用下面的构造从任何模块直接注册一个全局作用域的过滤器:

@@filename(app.module.ts)
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

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

Catch everything

为了可以不做到任意一个未处理的异常,将@Catch()的阐述设置为空。

举个例子,下面我们有一个与平台无关的代码,因为它使用HTTP适配器来交付响应,而不直接使用任何平台特定的对象(Request和response)

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

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly httpAdapterHost: HttpAdapterHost) {}

  catch(exception: unknown, host: ArgumentsHost): void {
    // In certain situations `httpAdapter` might not be available in the
    // constructor method, thus we should resolve it here.
    const { httpAdapter } = this.httpAdapterHost;

    const ctx = host.switchToHttp();

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

    const responseBody = {
      statusCode: httpStatus,
      timestamp: new Date().toISOString(),
      path: httpAdapter.getRequestUrl(ctx.getRequest()),
    };

    httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
  }
}

Inheritance

通常,我们将创建完全自定义的异常过滤器,以满足您的应用程序需求。然而,在一些用例中,您可能只想简单地扩展内置的默认全局异常过滤器,并基于某些因素重写行为。

了将异常处理委托给基础过滤器,您需要扩展BaseExceptionFilter并调用继承的catch()方法。

@@filename(all-exceptions.filter.ts)
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    super.catch(exception, host);

上面的实现只是一个演示该方法的shell。扩展异常筛选器的实现将包括定制的业务逻辑,全局过滤器可以扩展基过滤器。这有两种方法。第一个方法是在实例化自定义全局过滤器时注入HttpAdapter引用:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const { httpAdapter } = app.get(HttpAdapterHost);
  app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));

  await app.listen(3000);
}
bootstrap();

未完待续。。。

Pipes

Guards

Interceptors

Custom route decorators

参考应用文档

参考文档:nestjs.bootcss.com/