模块层
这一层是使用@Module()
装饰器的类,它提供了元数据,Nest 用它来组织应用程序结构。我们有了控制层和服务层后,它们还无法运行,因为它们缺少一个组织。
实现代码
接下来,我们在src目录下创建module
文件夹,在其目录下创建AppModule.ts
文件,代码如下所示:
- controllers 是一个数组类型的数据,我们把controller层的控制器在这里一一引入即可。
- providers 也是一个数组类型的数据,我们把service层的服务在这里一一引入即可。
import { Module } from "@nestjs/common"; import { AppController } from "../controller/AppController"; import { AppService } from "../service/AppService"; @Module({ imports: [], controllers: [AppController], providers: [AppService] }) export class AppModule {}
有关controllers与providers的详细介绍,请移步:Nest-@module
配置入口文件
接下来,我们在src目录下,创建main.ts
文件,它的代码如下所示:
- 导入AppModule,使用NestFactory来创建实例
- 将3000端口设为本项目的监听
import { NestFactory } from "@nestjs/core"; import { AppModule } from "./module/AppModule"; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();
最后,我们运行package.json中的start:dev
命令,在浏览器访问http://127.0.0.1:3000
即可访问该项目。
image-20220114230042606
验证控制层创建的控制器
接下来,我们来验证下前面在AppController.ts
中写的两个方法是否能正常运行。
验证Get方法
我们先来验证下get请求的访问情况,在浏览器访问
http://127.0.0.1:3000/home/getTitle?id=12
,客户端的界面如下所示:
image-20220114230439191
服务端同样也会输出客户端在地址栏所传的id,如下所示:
image-20220114230550220
验证Post方法
我们需要使用postman来测试post方法能否正常访问,假设你已经安装好了postman,我们新建一个请求,写入地址http://127.0.0.1:3000/home/setTitle
,访问结果如下所示:
image-20220114230935445
同样的,服务端也会收到我们在http body中所传的json数据,如下所示:
image-20220114231123801
DTO层(处理客户端参数)
在前面的例子中,我们获取客户端的参数都是直接写在控制器内每个方法的参数中的,这样做引发的问题有:
- 会降低代码的可读性,一大串参数写在方法里很不优雅。
- 当很多方法都都需要传入相同参数时,要写很多重复代码,可维护性大大降低。
- 参数的有效性验证需要写在控制器内的方法中,会产生冗余代码。
DTO层的作用就是解决上述问题的,我们用class
来处理客户端传入的参数。
实现代码
我们在src目录下创建DTO文件夹,在其目录下创建AppDto.ts
文件,代码如下所示:
export class AppDto { public id: string; public title: string; public name: string; } export class GetNameDto extends AppDto { public type: string; }
随后,我们在AppController.ts
中的方法里使用即可,代码如下所示:
import { AppDto, GetNameDto } from "../dto/AppDto"; @Controller("home") export class AppController { @Post("setTitle") setTitle(@Body() data: AppDto): { code: number; data: null | string; msg: string; } { // 其他代码省略 } @Get("getName") getName(@Body() data: GetNameDto): { code: number; data: null | string; msg: string; } { // 其他代码省略 } }
完成上述操作后,我们就成功解决了1,2问题。由于参数的接收是采用类实现的,因此我们可以利用继承来避免冗余代码。
使用管道验证参数的有效性
接下来,我们使用管道来解决第3个问题,在nest官网中,它提供了8个开箱即用的内置管道,此处我们需要用它的ValidationPipe
管道来验证参数。
根据文档所述,在使用前我们需要先绑定管道,官网给出了两种方法:
- 绑在 controller 或是其方法上,我们使用
@UsePipes()
装饰器并创建一个管道实例,并将其传递给 Joi 验证。 - 在入口处将其设置为全局作用域的管道,用于整个应用程序中的每个路由处理器。
此处我们使用全局作用域的管道,修改main.ts文件,代码如下所示:
import { NestFactory } from "@nestjs/core"; import { AppModule } from "./module/AppModule"; import { ValidationPipe } from "@nestjs/common"; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap();
有关管道的具体原理请移步:nest-绑定管道
随后,我们即可在dto层中使用它的相关装饰器来校验参数了,AppDto.ts
的部分代码如下所示:
import { IsString, MinLength } from "class-validator"; export class AppDto { @MinLength(5) @IsString() public id!: string; @IsString() public title!: string; @IsString() public name!: string; } export class GetNameDto extends AppDto { @IsString() public type!: string; }
最后,我们使用postman来测试下是否生效,如下所示:
- 传入了一个number类型的id
- 没传name参数
服务端返回了400错误,并告知了错误原因。
image-20220116221632391
因为我们将参数的非空验证交给了装饰器,我们在dto类中,就需要用
!:
操作符来断言某个参数一定有值。
我们从
class-validator'
包中引入了string类型的验证装饰器,它还能验证其它类型,感兴趣的开发者请移步:class-validator#usage
VO层(返回给客户端的视图)
通常情况下,我们返回给客户端的字段是固定的,在本文前面的controller层中,两个方法我们都返回了code
、data
、msg
这三个字段,只是数据不同。那么我们就应该把它封装起来,将数据作为参数传入,这样就大大的提高了代码的可维护性,也就是我们所说的VO层。
封装工具类
我们在src目录下创建VO
文件夹,在其目录下创建ResultVO.ts
文件,代码如下所示:
- 简单创建了一个类,添加了三个字段
- 为每个字段写了get和set方法
export class ResultVO<T> { private code!: number; private msg!: string; private data!: T | null; public getCode(): number { return this.code; } public setCode(value: number): void { this.code = value; } public getMsg(): string { return this.msg; } public setMsg(value: string): void { this.msg = value; } public getData(): T | null { return this.data; } public setData(value: T | null): void { this.data = value; } }
随后,我们在src目录下创建utils文件夹,在其目录下创建VOUtils.ts
文件,封装常用方法,便于其他层直接调用,代码如下所示:
- 我们封装了
success
与error
方法 - 成功时,传入data进来
- 失败时,传入code与msg告知客户端错误原因
// 返回给调用者的视图结构 import { ResultVO } from "../VO/ResultVO"; export class VOUtils { public static success<T>(data?: T): ResultVO<T> { const resultVo = new ResultVO<T>(); resultVo.setCode(0); resultVo.setMsg("接口调用成功"); resultVo.setData(data || null); return resultVo; } public static error(code: number, msg: string): ResultVO<null> { const resultVo = new ResultVO<null>(); resultVo.setCode(code); resultVo.setMsg(msg); return resultVo; } }
注意:
success
方法支持传入的参数是任意类型的,实际的业务需求中,data这一层会很复杂,你在实际使用时,可以根据具体的业务需求创建对应业务的vo类,然后对其进行实例化,为每个字段赋值。最后在调用success方法时将你实例化后的对象传入即可。
在业务代码中使用
随后,我们就可以在service
层来使用我们创建好的工具类了,示例代码如下所示:
import { VOUtils } from "../utils/VOUtils"; @Injectable() export class AppService implements AppInterface { // 其它代码省略 setTitle(): VOUtils { return VOUtils.success("标题设置成功"); } }
接口调用结果如下所示:
image-20220116231739210
类型层
我们在写业务代码时,会碰到许许多多的Object
类型的数据,通常情况下我们会给每个字段定义具体的类型,此时我们就需要将所有的类型放在一起,方便维护,此处我的做法是在src目录下创建type文件夹,将所有的类型定义都放在这个文件夹里,代码如下所示:
- 创建了一个type文件夹
- type文件夹下创建了
AppDataType.ts
文件,用于存放所有类型
export type book = { title: string; author: string; time: string; updateTime: string; }; export interface specialBook extends book { id: number; createTime: string; }
注意:所有的类型定义我们都用type关键词来定义,使用的时候直接导入即可,当我们要继承某个类型时,就必须要使用interface关键词了。
枚举层
我们写业务代码时,肯定会遇到各种异常状况,当服务端发生异常时,我们就需要在VO层返回错误信息与状态码,如果我们直接将数据写在方法里,后期需要修改时,将会是一件很头痛的事情。那么,当我们把这些数据统一在枚举层进行定义,在业务代码中直接使用我们定义好的枚举,这个问题就迎刃而解了。
我们在src目录下创建enum
文件夹,在其文件夹下创建AppEnum.ts
文件,代码如下所示:
- NOTFOUND 表示错误码
- NOTFOUND_DESCRIPTION 表示错误码的描述信息
export enum AppEnum { NOTFOUND = -1, NOTFOUND_DESCRIPTION = "未找到相关人物" }
随后,我们在业务代码使用即可,如下所示:
@Injectable() export class AppService implements AppInterface { // 其它代码省略 getName(): VOUtils { return VOUtils.error(AppEnum.NOTFOUND, AppEnum.NOTFOUND_DESCRIPTION); } }
注意:typescript中的枚举不能像Java一样在定义的时候就设置相关的描述信息,所以此处只能选择曲线救国的方式在定义错误吗的时候多定义一个以__DESCRIPTION结尾的枚举。
项目代码
本文所使用的完整代码,请移步项目的GitHub仓库:nest-project
写在最后
至此,文章就分享完毕了。
我是神奇的程序员,一位前端开发工程师。
如果你对我感兴趣,请移步我的个人网站,进一步了解。
- 公众号无法外链,如果文中有链接,可点击下方阅读原文查看😊