WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
46 stars 10 forks source link

Nestjs #137

Open WangShuXian6 opened 2 years ago

WangShuXian6 commented 2 years ago

Nest

https://nestjs.com

https://docs.nestjs.cn/8/introduction

Nest (NestJS) 是一个用于构建高效、可扩展的Node.js服务器端应用程序的框架

使用 TypeScript 构建并完全支持TypeScript

结合了 OOP(面向对象编程)、FP(函数式编程)和 FRP(函数式反应式编程)的元素

安装

npm i -g @nestjs/cli

nest new project-name

npm run start

npm run start:dev

核心文件

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

概述

Nest.js 是一个 Node.js 的后端框架,它对 express 等 http 平台做了一层封装,解决了架构问题。 它提供了 express 没有的 MVC、IOC、AOP 等架构特性,使得代码更容易维护、扩展。

MVC

MVC 是 Model View Controller 的简写。 MVC 架构下,请求会先发送给 Controller,由它调度 Model 层的 Service 来完成业务逻辑,然后返回对应的 View。

image

DI(dependency inject)

这种思想叫做 IOC(Inverse Of Control)

@Controller 装饰器用来声明 Controller Service 会用 @Injectable 装饰器来声明

通过 @Controller@Injectable 装饰器声明的 class 会被 Nest.js 扫描,创建对应的对象并加到一个容器里,这些所有的对象会根据构造器里声明的依赖自动注入,也就是 DI(dependency inject),这种思想叫做 IOC(Inverse Of Control)。

IOC 架构的好处是不需要手动创建对象和根据依赖关系传入不同对象的构造器中,一切都是自动扫描并创建、注入的。

AOP (Aspect Oriented Programming)面向切面编程

一个请求过来,可能会经过 Controller(控制器)、Service(服务)、Repository(数据库访问) 的逻辑: image

如果想在这个调用链路里加入一些通用逻辑该怎么加呢?比如日志记录、权限控制、异常处理等。

容易想到的是直接改造 Controller 层代码,加入这段逻辑。 这样可以,但是不优雅,因为这些通用的逻辑侵入到了业务逻辑里面。 能不能透明的给这些业务逻辑加上日志、权限等处理呢?

那是不是可以在调用 Controller 之前和之后加入一个执行通用逻辑的阶段呢?

比如这样: image

这样的横向扩展点就叫做切面,这种透明的加入一些切面逻辑的编程方式就叫做 AOP (面向切面编程)。

AOP 的好处是可以把一些通用逻辑分离到切面中,保持业务逻辑的存粹性,这样切面逻辑可以复用,还可以动态的增删

其实 Express 的中间件的洋葱模型也是一种 AOP 的实现,因为你可以透明的在外面包一层,加入一些逻辑,内层感知不到。

而 Nest.js 实现 AOP 的方式更多,一共有五种,包括 Middleware、Guard、Pipe、Inteceptor、ExceptionFilter:

Middleware

Nest.js 基于 Express 自然也可以使用中间件,但是做了进一步的细分,分为了全局中间件和路由中间件:

这个是直接继承了 Express 的概念

全局中间件就是 Express 的那种中间件,在请求之前和之后加入一些处理逻辑,每个请求都会走到这里:

const app = await NestFactory.create(AppModule);
app.use(logger); // 全局路由应用中间件
await app.listen(3000);

路由中间件则是针对某个路由来说的,范围更小一些:

export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(LoggerMiddleware) //中间件
.forRoutes("cats"); //限制路由范围
}
}

Guard 路由守卫

可以用于在调用某个 Controller 之前判断权限,返回 true 或者 flase 来决定是否放行: image

创建 Guard 的方式


@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
>Guard 要实现 CanActivate 接口,实现 canActive 方法,可以从 context 拿到请求的信息,然后做一些权限验证等处理之后返回 true 或者 false。

>通过 `@Injectable` 装饰器加到 IOC 容器中,然后就可以在某个 Controller 启用了:
```ts
@Controller("cats")
@UseGuards(RolesGuard)
export class CatsController {}

Controller 本身不需要做啥修改,却透明的加上了权限判断的逻辑,这就是 AOP 架构的好处。

而且,就像 Middleware 支持全局级别和路由级别一样,Guard 也可以全局启用:

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

Guard 可以抽离路由的访问控制逻辑,但是不能对请求、响应做修改,这种逻辑可以使用 Interceptor:

Interceptor 拦截器

可以在目标 Controller 方法前后加入一些逻辑: image 创建 Inteceptor 的方式 image

Interceptor 要实现 NestInterceptor 接口,实现 intercept 方法,调用 next.handle() 就会调用目标 Controller,可以在之前和之后加入一些处理逻辑。

Controller 之前之后的处理逻辑可能是异步的。Nest.js 里通过 rxjs 来组织它们,所以可以使用 rxjs 的各种 operator。

Interceptor 支持每个路由单独启用,只作用于某个 controller,也同样支持全局启用,作用于全部 controller: image image

除了路由的权限控制、目标 Controller 之前之后的处理这些都是通用逻辑外, 对参数的处理也是一个通用的逻辑,所以 Nest.js 也抽出了对应的切面,也就是 Pipe:

Pipe 管道

用来对参数做一些验证和转换: image

创建 Pipe 的方式 image

Pipe 要实现 PipeTransform 接口,实现 transform 方法,里面可以对传入的参数值 value 做参数验证, 比如格式、类型是否正确,不正确就抛出异常。也可以做转换,返回转换后的值。

内置的有 8 个 Pipe

ValidationPipe ParseIntPipe ParseBoolPipe ParseArrayPipe ParseUUIDPipe DefaultValuePipe ParseEnumPipe ParseFloatPipe

Pipe 可以只对某个路由生效,也可以对每个路由都生效: image image

不管是 Pipe、Guard、Interceptor 还是最终调用的 Controller,过程中都可以抛出一些异常,如何对某种异常做出某种响应呢?

这种异常到响应的映射也是一种通用逻辑,Nest.js 提供了 ExceptionFilter 来支持:

ExceptionFilter

ExceptionFilter 可以对抛出的异常做处理,返回对应的响应: image

创建 ExceptionFilter image

首先要实现 ExceptionFilter 接口,实现 catch 方法,就可以拦截异常了,但是要拦截什么异常还需要用 @Catch 装饰器来声明,拦截了异常之后,可以异常对应的响应,给用户更友好的提示。

当然,也不是所有的异常都会处理,只有继承 HttpException 的异常才会被 ExceptionFilter 处理,

Nest.js 内置了很多 HttpException 的子类:

BadRequestException UnauthorizedException NotFoundException ForbiddenException NotAcceptableException RequestTimeoutException ConflictException GoneException PayloadTooLargeException UnsupportedMediaTypeException UnprocessableException InternalServerErrorException NotImplementedException BadGatewayException ServiceUnavailableException GatewayTimeoutException

当然,也可以自己扩展: image

Nest.js 通过这样的方式实现了异常到响应的对应关系,代码里只要抛出不同的 HttpException,就会返回对应的响应,很方便。

ExceptionFilter 也可以选择全局生效或者某个路由生效:

某个路由: image

全局: image

几种 AOP 机制的顺序

Middleware、Guard、Pipe、Interceptor、ExceptionFilter 都可以透明的添加某种处理逻辑到某个路由或者全部路由,这就是 AOP 的好处。

它们之间的顺序关系对应的源码是这样的: image

很明显,进入这个路由的时候,会先调用 Guard,判断是否有权限等,如果没有权限,这里就抛异常了: image

抛出的 HttpException 会被 ExceptionFilter 处理。

如果有权限,就会调用到拦截器,拦截器组织了一个链条,一个个的调用,最后会调用的 controller 的方法: image

调用 controller 方法之前,会使用 pipe 对参数做处理: image image

会对每个参数做转换: image

ExceptionFilter 的调用时机很容易想到,就是在响应之前对异常做一次处理。

而 Middleware 是 express 中的概念,Nest.js 只是继承了下,那个是在最外层被调用。

这就是这几种 AOP 机制的调用顺序。把这些理清楚,就算是对 Nest.js 有很好的掌握了。

总结

Nest.js 基于 express 这种 http 平台做了一层封装,应用了 MVC、IOC、AOP 等架构思想。

MVC 就是 Model、View Controller 的划分,请求先经过 Controller,然后调用 Model 层的 Service、Repository 完成业务逻辑,最后返回对应的 View。

IOC 是指 Nest.js 会自动扫描带有 @Controller、@Injectable 装饰器的类,创建它们的对象,并根据依赖关系自动注入它依赖的对象,免去了手动创建和组装对象的麻烦。

AOP 则是把通用逻辑抽离出来,通过切面的方式添加到某个地方,可以复用和动态增删切面逻辑。

Nest.js 的 Middleware、Guard、Interceptor、Pipe、ExceptionFileter 都是 AOP 思想的实现,只不过是不同位置的切面,它们都可以灵活的作用在某个路由或者全部路由,这就是 AOP 的优势。

我们通过源码来看了它们的调用顺序,Middleware 是 Express 的概念,在最外层,到了某个路由之后,会先调用 Guard,Guard 用于判断路由有没有权限访问,然后会调用 Interceptor,对 Contoller 前后扩展一些逻辑,在到达目标 Controller 之前,还会调用 Pipe 来对参数做验证和转换。所有的 HttpException 的异常都会被 ExceptionFilter 处理,返回不同的响应。

Nest.js 就是通过这种 AOP 的架构方式,实现了松耦合、易于维护和扩展的架构。

WangShuXian6 commented 2 years ago

控制器 controllers

控制器负责处理传入的请求并将响应返回给客户端。

CRUD 生成器 CRUD generator

nest g resource [name]

路由

@Controller()装饰器 是定义一个基本控制器所必须的。

我们将指定一个可选的路径前缀为cats。 在@Controller()装饰器中使用路径前缀可以让我们轻松地将一组相关的路由分组,并将重复的代码降到最低。

要使用 CLI 创建控制器,只需执行nest g controller cats命令

// cats.controller.tsJS

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

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

@Get() HTTP请求方法装饰器告诉Nest为HTTP请求的特定端点创建一个处理程序 处理程序的路径是通过连接为控制器声明的(可选)前缀,以及方法装饰器中指定的任何路径来确定。 由于我们已经为每个路由(cats)声明了一个前缀,并且没有在装饰器中添加任何路径信息,Nest将把GET /cats请求映射到这个处理器

路径包括可选的控制器路径前缀和请求方法装饰器中声明的任何路径字符串。 例如,customers的路径前缀与装饰器@Get('profile')相结合,将产生一个GET /customers/profile这样的路由映射请求。

例如,我们可以选择将一组管理与客户实体互动的路由归入路由/customers。 在这种情况下,我们可以在@Controller()装饰器中指定路径前缀customers, 这样我们就不必为文件中的每个路由重复这部分路径。

请求对象 Request object

处理程序经常需要访问客户端的请求细节。 Nest提供了对底层平台(默认为Express)的请求对象的访问。 我们可以通过在处理程序的签名中添加@Req()装饰器来指示Nest注入请求对象来访问它。

为了利用express类型(如上面的参数示例request: Request),安装@types/express包。

//cats.controller.tsJS

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

请求对象 装饰器

请求对象代表了HTTP请求,并有请求查询字符串、参数、HTTP头和正文的属性(在此处阅读更多内容)。 在大多数情况下,没有必要手动抓取这些属性。 我们可以使用专门的装饰器来代替,比如@Body()@Query(),这些装饰器开箱即用。 下面是一个所提供的装饰器的列表,以及它们所代表的普通平台特定对象。

@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

为了与底层HTTP平台(如Express和Fastify)的类型兼容,Nest提供了@Res()@Response()装饰器。 @Res()@Response() 的简单别名。两者都直接暴露了底层的本地平台响应对象接口。 当使用它们时,你也应该导入底层库的类型(例如,@types/express)以充分利用。 请注意,当你在方法处理程序中注入@Res()@Response()时,你将Nest放入该处理程序的库特定模式中,并且你将负责管理响应。 当这样做时,你必须通过调用响应对象(如res.json(...)res.send(...))来发出某种响应,否则 HTTP 服务器将挂起。

要了解如何创建自己的自定义装饰器,请访问本章

资源 Resources#

一个创建新记录的端点


//cats.controller.tsJS

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 为所有标准 HTTP 方法提供装饰器:`@Get()`、`@Post()`、`@Put()`、`@Delete()`、`@Patch()`、`@Options()`和`@Head()`. 此外,`@All()`定义一个处理所有这些的端点。

## 路由通配符 Route wildcards[#](https://docs.nestjs.com/controllers#route-wildcards)

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

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

'ab*cd'路由路径将匹配abcd,ab_cd,abecd等。 字符?,+, *()可以在路由路径中使用,并且是它们的正则表达式对应物的子集。 连字符 ( -) 和点 ( .) 由基于字符串的路径逐字解释。

状态码 Status code#

如前所述,响应状态代码默认总是200,除了POST请求是201。 我们可以通过在处理程序级别添加@HttpCode(...)装饰器来轻松改变这一行为。

@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

HttpCode从@nestjs/common包中 导入。

通常,您的状态代码不是静态的,而是取决于各种因素。在这种情况下,您可以使用特定于库的响应(使用 注入@Res())对象(或者,如果出现错误,则抛出异常)。

响应标头 Headers

要指定一个自定义的响应头,你可以使用@Header()装饰器或一个库特定的响应对象(并直接调用res.header())。

@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}

Header从@nestjs/common包中 导入。

重定向 Redirection#

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

@Redirect()需要两个参数,urlstatusCode,都是可选的。如果省略的话,statusCode的默认值是302(Found)。

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

有时您可能希望动态确定 HTTP 状态代码或重定向 URL。通过从具有以下形状的路由处理程序方法返回一个对象来做到这一点:

{
  "url": string,
  "statusCode": number
}

返回值将覆盖传递给@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#

当您需要接受动态数据作为请求的一部分时(例如,GET /cats/1获取带有 id 的 cat ),具有静态路径的路由将不起作用1。 为了定义带参数的路由,我们可以在路由的路径中添加路由参数标记,以捕获请求 URL 中该位置的动态值。 下面装饰器示例中的路由参数标记@Get()演示了这种用法。 可以使用@Param()装饰器访问以这种方式声明的路由参数,装饰器应添加到方法签名中。

@Get(':id')
findOne(@Param() params): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

@Param()被用来装饰一个方法参数(上面例子中的params),并使路由参数作为该方法主体中被装饰的方法参数的属性可用。 正如上面的代码所见,我们可以通过引用params.id来访问id参数。 你也可以向装饰器传递一个特定的参数标记,然后在方法主体中直接引用路由参数的名称。

Param从@nestjs/common包中 导入


@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a #${id} cat`;
}

## 子域路由 Sub-Domain Routing
>`@Controller`装饰器可以接受一个`host`选项,要求传入请求的HTTP主机与某些特定值相匹配。
```ts
@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

由于Fastify缺乏对嵌套路由器的支持,所以在使用子域路由时,应该使用(默认)Express 适配器。

与路由路径类似,hosts选项可以使用令牌来捕获主机名称中该位置的动态值。 下面@Controller()装饰器例子中的主机参数令牌展示了这种用法。 以这种方式声明的主机参数可以使用@HostParam()装饰器进行访问,它应该被添加到方法签名中。


@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}

## Scopes
>对于来自不同编程语言背景的人来说,在 Nest 中得知几乎所有内容都是在传入请求之间共享的,这可能是出乎意料的。我们有一个到数据库的连接池、具有全局状态的单例服务等。请记住,Node.js 不遵循请求/响应多线程无状态模型,其中每个请求都由单独的线程处理。因此,使用单例实例对我们的应用程序来说是完全安全的。

>然而,当控制器的基于请求的生命周期可能是期望的行为时,存在边缘情况,例如 GraphQL 应用程序中的每个请求缓存、请求跟踪或多租户。[在此处](https://docs.nestjs.com/fundamentals/injection-scopes)了解如何控制范围。

## 异步 Asynchronicity
>每个异步函数都必须返回一个Promise. 这意味着您可以返回 Nest 能够自行解析的延迟值。
```ts
//cats.controller.tsJS

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

由于能够返回 RxJS可观察流,Nest 路由处理程序更加强大。Nest 将自动订阅下面的源并获取最后一个发出的值(一旦流完成)。


cats.controller.tsJS

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


## 请求有效载荷 Request payloads[#](https://docs.nestjs.com/controllers#request-payloads)
>使用`@Body()`装饰器 接受任何客户端参数

### DTO(数据传输对象)
>但首先(如果你使用TypeScript),我们需要确定DTO(数据传输对象)模式。
>DTO是一个定义了数据如何在网络上发送的对象。
>我们可以通过使用TypeScript接口,或通过简单的类来确定DTO模式。
>有趣的是,我们在这里推荐使用类。为什么呢?类是JavaScript ES6标准的一部分,因此它们在编译后的JavaScript中被保留为真实的实体。
>另一方面,由于TypeScript接口在转译过程中被移除,Nest不能在运行时引用它们。
>这一点很重要,因为像管道这样的功能在运行时可以访问变量的元类型,从而实现更多的可能性。

#### 创建CreateCatDto类
```ts
create-cat.dto.tsJS

export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

它只有三个基本属性。此后我们可以在CatsController中使用新创建的DTO。


cats.controller.tsJS

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

>我们ValidationPipe可以过滤掉不应被方法处理程序接收的属性。在这种情况下,我们可以将可接受的属性列入白名单,任何未包含在白名单中的属性都会自动从结果对象中剥离。
>在CreateCatDto示例中,我们的白名单是name、age和breed属性。[在这里](https://docs.nestjs.com/techniques/validation#stripping-properties)了解更多。

## 完整资源样本
>下面是一个使用几个可用装饰器来创建基本控制器的示例。这个控制器公开了几个方法来访问和操作内部数据

```ts
cats.controller.tsJS

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仍然不知道CatsController的存在,因此不会创建这个类的实例。

控制器总是属于一个模块,这就是为什么我们在@Module()装饰器中包含控制器数组。 因为除了根AppModule,我们还没有定义任何其他模块,所以我们将用它来介绍CatsController。

app.module.tsJS

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

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

我们使用@Module()装饰器将元数据附加到模块类,Nest现在可以很容易地反射出哪些控制器必须被安装。

特定于库的方法

到目前为止,我们已经讨论了操作响应的Nest标准方式。 操作响应的第二种方式是使用库的特定响应对象。 为了注入一个特定的响应对象,我们需要使用@Res()装饰器。 为了显示差异,让我们把CatsController重写成以下内容。


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

>虽然这种方法是可行的,而且事实上通过提供对响应对象的完全控制,在某些方面确实有更大的灵活性(头文件的操作、库的特定功能等等),但应该谨慎使用。一般来说,这种方法不那么明确,而且确实有一些缺点。主要的缺点是,你的代码变得依赖于平台(因为底层库在响应对象上可能有不同的API),而且更难测试(你必须模拟响应对象,等等)。

>另外,在上面的例子中,你失去了与依赖Nest标准响应处理的Nest功能的兼容性,如拦截器和`@HttpCode() `/ `@Header() `装饰器。为了解决这个问题,你可以将passthrough选项设置为true,如下所示。
```ts
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

现在,你可以与本地响应对象进行交互(例如,根据某些条件设置cookies或头信息),但把其他的事情留给框架。