Nestjs学习笔记
Nest学习笔记
前言
学习Nestjs更多的处于兴趣,公司项目中有使用nest,从代码结构看感觉跟springboot的写法类似,我也在自己的部分demo项目中应用了Nestjs+express作为框架进行开发,因此开始学习Nest的相关知识
介绍
在学习Nestjs之前需要有一定的TypeScript的使用或学习经验,虽然Nest支持使用原生纯js进行书写,但是官方示例以及cli工具生成的代码都是ts的,笔记中的相关代码部分我也会应用ts进行记录。
支持版本:Node.js(>= 10.13.0, v13版本除外)
- 版本查看命令如下:
1 | $ node -v |
注:如果电脑没有node环境,或没有node相关基础请移步node教程
- 安装Nest Cli工具及创建项目:
1 | npm i -g @nestjs/cli |
通过cli创建的项目中包含以下核心文件:
src
|- app.controller.spec.ts (对于基本控制器的单元测试样例)
|- app.controller.ts (带有单个路由的基本控制器示例)
|- app.module.ts (应用程序的根模块)
|- app.service.ts (带有单个方法的基本服务)
|- main.ts (应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例)作为渐进式框架兼容express:
创建应用时可以直接指定需要使用的HTTP平台
1 | const app = await NestFactory.create<NestExpressApplication>(AppModule); |
- 运行方式
npm run start
Controller
Controller主要用于通过分组路由对传入的请求进行处理并向客户端返回响应
创建与路由
cli快捷命令:
nest g controller project-name
注解@Controller()
中存在可选参数路由前缀,可以通过设置路由前缀对文件中的路由进行分组,例如@Controller(index)
必须通过localhost:3000/index
来请求,这样就不需要在内部方法中重指定路由前缀了,官方示例:
1 | /* cats.controller.ts */ |
路由路径:用于指定请求的处理方法,可以通过
/路由前缀/装饰器路由信息
的方式生成路由映射装饰器:即
@Get()
、@Post()
等在内的HTTP请求方法装饰器,用于指定端点对应的HTTP请求方法
路由中支持使用通配符*
用于匹配任何字符组合即字符 ?
、+
、 *
以及 ()
是它们的正则表达式对应项的子集。连字符-
和点.
按字符串路径逐字解析。
1 | 'ab*cd') ( |
上述路由可以匹配abcd
、ab_cd
、abecd
等
Request
Nest在提供了一些方案来处理请求细节,它提供了对于请求对象(request)的访问方式,可以通过处理函数中的@Req()
装饰器,指示Nest将请求对象注入处理程序,当然在正常的程序书写中,我们也不会直接去处理整个request对象相反我们更多的是处理HTTP header
和Http body
的属性,因此Nest也提供了@Body
和@Query
等可以开箱即用的装饰器
详细对照请参考()[https://docs.nestjs.cn/9/controllers]
1 | 'index') ( |
请求中我们总会处理各种状态码,可以通过在处理函数外添加@HttpCode()
注解指定需要的状态码
我们可以使用@Header()
来指定自定义的响应头
如果在一些特定情况下需要将响应重定向到指定的URL
(例如404的时候),可以使用@Redirect()
制定需要的URL
@Redirect()
中有两个可选参数,一个就是url
,另一个是statusCode
,如果省略第二个参数则会默认使用302
状态码
当我们在请求处理中需要接收动态数据的时候@Param
可以通过下述两种方式进行获取所需的路由参数,访问方法为GET /cats/1
1 | ':id') ( |
异步数据
每一个异步函数都必须返回一个Promise
,因此Nest支持使用异步函数(Async / await
),Nest会自动解析这个被返回的Promise
值
请求负载
在POST
请求中我们可以使用@Body()
获取请求传入的参数,如果应用Typescript
则需要确定DTO
(数据传输对象),它定义了需要数据的格式,可以通过ts的interface
或class
来定义DTO
模式,Nest更推荐使用class
来定义
1 | /* |
通过定义可以直接在下述DemoController
中通过不同的两种方式使用创建的DTO
:
1 | /* demo.controller.ts */ |
注册控制器
当Controller已经准备就绪后,我们需要告知Nest该Controller的存在,而Controller总是属于Module的,所以我们可以看到@Module()
装饰器中包含了controllers
数组
因此可以通过以下方式进行定义:
1 | /* app.module.ts */ |
Provide
CLI快捷命令:
$ nest g service cats
Provide
是一个使用@Injectble()
装饰器注释的类,它的设计理念源于控制反转(IOC)中依赖注入的特性
Nest 的分层借鉴自 Spring,更细化。随着代码库的增长 MVC 模式中 Modal 和 Controller 会变得含糊不清,导致难于维护。
Service
先从一个服务开始了解Provide
,先创建一个简单的负责数据存储与检索的服务DemoService
开始
1 | /* cats.service.ts */ |
CatsService
是通过类构造函数注入的。注意这里使用了私有的只读语法。这意味着我们已经在同一位置创建并初始化了 catsService
成员。
上面示例中constructor(private catsService: CatsService) {}
就是依赖注入的一种体现,Nest
将catsService
创建后会返回一个实例来解析CatsService
Provider通常拥有与主体程序同步的生命周期,即作用域
在constructor
的参数中可以使用@Optional()
标明参数可选
对于依赖注入不仅仅只有上述通过构造函数注入的方式,但是如果一个顶级类需要依赖于一个或多个providers
时这种方式就会变得很麻烦,因此这时我们可以使用基于属性的注入,即应用@Inject()
装饰器
1 | import { Injectable, Inject } from '@nestjs/common'; |
注意:如果您的类没有扩展其他提供者,你应该总是使用基于构造函数的注入。
当我们在controller
中注册了服务的使用者以后,我们需要在Nest
中注册这个服务,以便于其可以正确地执行注入
我们可以编辑模块文件app.module.ts
,然后将服务添加到@Module()
装饰器的providers
数组中。
Module
CLI快捷命令:
$ nest g module cats
Module是一个具有@Module
装饰器的类,Nest通过Module形成了整体的应用程序结构
每一个Nest程序至少有一个Module,被称为根模块,当应用程序比较小的时候它就应该是唯一的模块
@Module()
装饰器可以接收一个描述模块属性的对象:
1 | import { Module } from '@nestjs/common'; |
当我们将子模块导出后可以直接在根模块ApplicationModule
中进行导入
实际上每个模块都是一个共享模块,一旦创建就能被任意的模块重复使用,当我们需要在几个模块之间共享一个Service
实例,那么我们只需要将该Service
放到exports
数组中即可
之后每个导入此Module
的模块都可以直接访问该`Service
provider
可以通过依赖注入写入Module
中但是不能反过来将Module
注入provider
全局模块
有些模块可能由于一些原因需要封装一些公用的东西(例如数据库连接、helper等),那这个时候讲究需要用到全局模块
1 | import { Module, Global } from '@nestjs/common'; |
@Global
装饰器可以使模块编程全局作用于,而全局模块只需要在核心模块中注册一次,如此则不需要再在其他需要使用该全局模块的地方再次引入了
动态模块
动态模块是Nest
中的一个强大功能,这些模块可以动态注册和配置提供程序,下面是官方提供的一个动态模块DatabaseModule
的定义示例:
1 | // 方法1 |
forRoot函数可以返回一个动态模块,这个模块可以使同步的也可以是异步的
上述方式通过默认的方式封装了一个Connection
的provider
,此外还通过将一个包含entities
和options
的对象传入forRoot()
方法中(类似一个存储库)
动态模块返回的属性扩展(非覆盖方式)了通过@Module
装饰器定义的基础模块的元数据,这就是通过模块导出静态声明的Connection
提供程序和动态生成的存储库provider
的方式
如果需要再全局范围内注册动态模块则需要将上述return
中的global
属性设置为true
对于上述DatabaseModule
可以被导入,导入方式如下:
1 | import { Module } from '@nestjs/common'; |
如果需要重新导出动态模块则可以省略forRoot
方法调用
1 | import { Module } from '@nestjs/common'; |
中间件(Middleware)
中间件是一个在路由跳转之前调用的函数,这个函数可以访问request
和response
对象,以及应用程序的请求响应周期中的next()
中间件函数
这里的中间件实际上与Express
中的中间件相同
中间件函数可以执行如下任务:
- 执行任何代码
- 对
request/response
对象进行修改- 结束请求/响应周期
- 调用堆栈中的下一个中间件函数
- 如果在这个中间件函数中没有对请求/响应周期进行终止则需要调用
next()
方法传递到下一个中间件函数,否则这个请求/响应将被挂起
定义一个中间件函数应该在其具有@Injectable()
装饰器的类中实习NestMiddleware
接口,如果是一个函数则没有特殊要求
1 | import { Injectable, NestMiddleware } from '@nestjs/common'; |
中间件同样支持依赖注入
应用中间件
中间件不能在@Module
中列出,我们必须使用模块类的configure()
方法来进行设置,且必须实现NestModule
接口
1 | import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; |
在我们对中间件进行配置时可以将包含路径的对象和请求方法传入forRoutes()
方法,这样可以进一步地将中间件限制为特定的请求方法
如下导入RequestMethod
来引用所有需要的请求方法类型
1 | import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common'; |
路由通配符
在中间件的路由匹配中支持模式匹配,通配符的使用和正则类似,例如*
可以匹配任意字符,例如下方示例:
1 | forRoutes({ path: 'ab*cd', method: RequestMethod.ALL }); |
消费者
forRoutes()
不仅仅可以接收字符串、对象还能接收一个甚至多个控制类,大多数情况下都会传入一个控制器列表
1 | import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; |
如果在应用中间件中想要排除一些路由可以使用exclude()
方法进行操作,这个方法参数可以是一个字符串、多个字符串或者一个RouterInfo
对象
1 | consumer |
函数中间件
从上面的介绍中可以知道LoggerMiddleware
类非常简单,实际上他可以只使用一个简单的函数而这种类型我们称为函数式中间件
1 | export function logger(req, res, next) { |
定义后可以直接在appModule
中进行使用
1 | consumer.apply(logger).forRoutes(CatsController); |
上面我们也提到了可以同时引入多个中间件,方法如下
1 | consumer.apply(cors(), helmet(), logger).forRoutes(CatsController); |
全局中间件
我们可以使用INestApplication
实例中提供的use()
方法直接将中间件绑定到所有的已注册路由
1 | import {NestFactory} from "@nestjs/core"; |
异常过滤器
Nest中提供了内置的异常层,开箱即用,这个异常处理操作由内置的全局异常过滤器负责,该过滤器可以处理HttpException
的异常,如果异常无法被识别时,用户将收到如下`JSON
1 | { |
基础异常
Nest
中有一个内置的HttpException
类,最常用的就是在发生错误时发送标准的HTTP响应对象
在如下示例中会模拟一个异常
1 | () |
当触发请求时客户端就会收到如下响应
1 | { |
HttpException
构造函数有两个必要参数:
response
参数是一个JSON
响应体status
定义HTTP
状态码
JSON
响应体包含两个属性:
statusCode
:状态码message
:基于状态的错误描述
一般情况下我们不需要编写自定义异常,如果确实需要创建自定义异常则要创建自己的异常层次结构
1 | import {Get} from "@nestjs/common"; |
异常过滤器
异常过滤器可以赋予开发者对于异常层完全控制权
异常过滤器可以用来捕获作为HttpException
类实例的异常并设置自定义的响应逻辑,因此我们需要应用Request
和Response
对象来进行我们想要的操作
1 | import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'; |
所有的异常过滤器都必须实现通用的
ExceptionFilter<T>
接口
在过滤器编写完成后需要将过滤器绑定到controller
的create()
方法上
1 | // 方法1 |
上述两种在实际使用时更推荐使用类而非使用实例,这样可以减少对内存的占用
异常过滤器的作用域可以划分为三个级别:方法范围、控制器范围以及全局范围
方法范围和控制器范围都是在相关方法或Controller
上配置@UseFilters()
,而全局范围的配置可以如下使用:
1 | import {NestFactory} from "@nestjs/core"; |
通过上述useGlobalFilters()
方法配置时不会为网关和混合应用程序设置过滤器
下面这种办法可以注册一个全局范围的过滤器
1 | import { Module } from "@nestjs/common"; |
在编写异常过滤器时使用@Catch()
装饰器列表为空则默认捕获每一个未处理的异常
继承
通常我们将创建完全定制化的异常过滤器,如果想要重用一个已经实现了核心逻辑的异常过滤器,并重写部分方法,那么就需要用到继承
1 | import { Catch, ArgumentsHost } from '@nestjs/common'; |
Pipes(管道)
Pipes
是具有@Injectable()
装饰器的类,Pipes
应该实现PipeTransform
接口。
通常pipes
会处理来自控制器路由处理程序的参数,Nest会在调用这个方法之前插入一个管道,管道会先拦截方法的调用参数,进行预处理
Nest中有很多开箱即用的内置管道,下面会有将内置pipes
绑定在controller
上的示例
当管道内抛出异常时controller将不会继续执行
内置管道
Nest中一共有九个开箱即用的管道:
- ValidationPipe
- ParseIntPipe
- ParseFloatPipe
- ParseBoolPipe
- ParseArrayPipe
- ParseUUIDPipe
- ParseEnumPipe
- DefaultValuePipe
- ParseFilePipe
以上的几个管道都是从@nestjs/common
包中导出的
下面将通过ParseIntPage
来演示一下转换的应用场景
绑定管道
1 | ':id') ( |
这样就能确保findOne()
方法所接受的参数是一个数字,且在路由处理程序呗调用之前就被处理并抛出异常
GET localhost:3000/abc
这样请求后Nest就会放回相关异常
1 | { |
异常抛出后findOne
方法不会被继续执行,上述方法中我们传入的是一个类将实例化的部分交给框架进行处理也就是依赖注入,我们也可以选择传入一个实例
1 | ':id') ( |
同上述操作一样,其他的
Parse*
管道的使用方式一致
自定义管道
如上文说的Nest提供了很多强大的内置管道,但是有些清关我们也需要使用更加定制化的管道
以一个自定义的简单ValidationPipe
为例:
1 | import { PipeTranform, Injectable, ArgumentMetadata } from '@nestjs/common'; |
PipeTransform<T, R>
是管道必须实现的泛型接口,T是value类型,R是transform()
方法的返回类型
transform
方法有两个参数value
和metadata
,value
事当前处理的方法的参数,metadata
是当前处理的方法参数的元数据
1 | export interface ArgumentMetadata { |
type
:参数是一个body@Body()
,query@Query()
,param@Param()
或其他自定义参数metatype
: 数据元类型,如String
等data
传递给装饰器的字符串,例如@Body('string')
应用场景
Pipes
有两个典型的应用场景:
- 转换:将输入数据转换为需要的数据输出
- 验证:对输入的数据进行验证,如果验证成功则继续传递,否则抛出异常