🌘NestJS 学习 —— 入门
00 分钟
2023-4-6
2023-4-12
type
status
date
slug
summary
tags
category
icon
password
Edited
Apr 12, 2023 01:52 PM
Created
Apr 12, 2023 01:47 PM
原文

为什么选择Nest.js

前面也说了, 大家都说香啊~
其次,我之前也使用过Egg.js,19年使用的时候,感觉egg约束性比较强,但是对于内部统一规范还是有好处的,但现在2021了, 已经习惯了TS,但Egg.js没有原生提供的TypeScript支持, 开发时可使用egg-ts-helper 来帮助自动生成 d.ts 文件,这样第三方库的支持完全不受控制, 风险还是很大, 所有选择放弃了
说了这么多,接下来开始吧!文章主要包含以下内容:
notion image

初识 Nest.js

Nest.js官网介绍:
Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。
在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify !
Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块。
上面这段话刚开始并不能完全理解, 但是简单可以解读出来Nest.js的几个特点:
  • 原生支持TypeScript的框架
  • 可以基于Express也可以选择fastify, 如果你对Express非常熟练, 直接用它的API也是没问题的
至于其他看不懂,就暂时放一边, 因为不影响我们入门,后面深入学习后会再来分析。

项目创建

首先确定你已经安装了Node.js, Node.js 安装会附带npx和一个npm 包运行程序。要创建新的Nest.js 应用程序,请在终端上运行以下命令:
执行完创建项目, 会初始化下面这些文件, 并且询问你要是有什么方式来管理依赖包:
notion image
如果你有安装yarn,可以选择yarn,能更快一些,npm在国内安装速度会慢一些,我这里就用npm下载了。 这里省略一个漫长的等待过程~, 终于看到了它成功了(然后我又删除了,使用yarn,确实速度快了很多)
接下来按照提示运行项目:
notion image
这里说一下我安装的环境,Nest.js版本不同有些API会有差异
版本
Node.js
v12.16.1
npm
6.13.4
nest.js
8.1.4
typescript
4.3.5
注意: Nest.js 要求 Node.js(>= 10.13.0,v13 除外), 如果你的Node.js 版本不满足要求,可以通过nvm包管理工具安装符合要求的Node.js版本

项目结构

进入项目,看到的目录结构应该是这样的:
notion image
这里简单说明一下这些核心文件:
| app.controller.ts | 单个路由的基本控制器(Controller) | | app.controller.spec.ts | 针对控制器的单元测试 | | app.module.ts | 应用程序的根模块(Module) | | app.service.ts | 具有单一方法的基本服务(Service) | | main.ts | 应用程序的入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序的实例。 |

第一个接口

前面我们已经启动了服务, 那我们怎么查看呢, 首先就是找到入口文件main.ts
内容比较简单, 使用Nest.js的工厂函数NestFactory来创建了一个AppModule实例,启动了 HTTP 侦听器,以侦听main.ts 中所定义的端口。
监听的端口号可以自定义, 如果3000端口被其他项目使用,可以更改为其他的端口号 因为我的3000端口有别的项目在用, 所以修改成:9080,重新启动项目
我们打开浏览器访问http://localhost:9080地址:
这里看到的Hello World就是接口地址http://localhost:9080返回的内容, 不信我们也可以使用常用 Postman看看:
notion image
说明Nest.js创建项目默认就给写了一个接口例子,那就通过这个接口例子来看,我们应该怎么实现一个接口。
前边看到mian.ts中也没有别的文件引入, 只有AppModule, 打开src/app.module.ts:
AppModule是应用程序的根模块,根模块提供了用来启动应用的引导机制,可以包含很多功能模块。
.mudule文件需要使用一个@Module() 装饰器的类,装饰器可以理解成一个封装好的函数,其实是一个语法糖(对装饰器不了解的,可以看走近MidwayJS:初识TS装饰器与IoC机制)。@Module() 装饰器接收四个属性:providerscontrollersimportsexports
  • providers:Nest.js注入器实例化的提供者(服务提供者),处理具体的业务逻辑,各个模块之间可以共享(注入器的概念后面依赖注入部分会讲解);
  • controllers:处理http请求,包括路由控制,向客户端返回响应,将具体业务逻辑委托给providers处理;
  • imports:导入模块的列表,如果需要使用其他模块的服务,需要通过这里导入;
  • exports:导出服务的列表,供其他模块导入使用。如果希望当前模块下的服务可以被其他模块共享,需要在这里配置导出;
如果你是Vue或者React技术栈,初次接触Nest.js,可能会觉得很面生啊, 其实很正常,Nest.js的思维方式一开始确实不容易理解,但假如你接触过AngularJS,就会感到熟悉,如果你用过 Java 和 Spring 的话,就可能会想,这不是抄的 Spring boot嘛!
确实AngularJSSpringNest.js都是基于控制反转原则设计的,而且都使用了依赖注入的方式来解决解耦问题。如果你觉得一头雾水, 别急,这些问题后面深入学习都会一一讲解的。这里我们还是照葫芦画瓢,学一下Nest究竟怎么使用的。
app.module.ts中,看到它引入了app.controller.tsapp.service.ts,分别看一下这两个文件:
使用@Controller装饰器来定义控制器, @Get是请求方法的装饰器,对getHello方法进行修饰, 表示这个方法会被GET请求调用。
从上面,我们可以看出使用@Injectable修饰后的 AppService, 在AppModule中注册之后,在app.controller.ts中使用,我们就不需要使用new AppService()去实例化,直接引入过来就可以用。
至此,对于http://localhost:9080/接口返回的Hello World逻辑就算理清楚了, 在这基础上我们再详细的学习一下Nest.js中的路由使用。

路由装饰器

Nest.js中没有单独配置路由的地方,而是使用装饰器。Nest.js中定义了若干的装饰器用于处理路由。

@Controller

如每一个要成为控制器的类,都需要借助@Controller装饰器的装饰,该装饰器可以传入一个路径参数,作为访问这个控制器的主路径:
app.controller.ts文件进行修改
然后重新启一下服务,此时再去访问http://localhost:9080/会发现404了。
就是由于通过@Controller("app")修改这个控制器的路由前缀为app, 此时可以通过http://localhost:9080/app来访问。

HTTP方法处理装饰器

@Get@Post@Put等众多用于HTTP方法处理装饰器,经过它们装饰的方法,可以对相应的HTTP请求进行响应。同时它们可以接受一个字符串或一个字符串数组作为参数,这里的字符串可以是固定的路径,也可以是通配符。
继续修改app.controller.ts,看下面的例子:
由于修改了文件, 需要重启才能看到路由, 每次都重启简直就是噩梦,本来打算配置一个实时监听文件变化,发现Nest.js非常贴心的配置好了, 我们只要运行命令即可:
这样再修改什么内容, 保存后都会自动重启服务了。
这里要提一个关于路由匹配时的注意点, 当我们有一个put请求,路径为/app/list/user,此时,我们在app.controller.ts控制器文件中增加一个方法:
你觉得这个路由会被匹配到吗?我们测试一下:
notion image
发现/app/list/user匹配到的并不是updateUser方法, 而是update方法。这就是我要说的注意点。
如果因为在匹配过程中, 发现@Put("list/:id")已经满足了,就不会继续往下匹配了,所以 @Put("list/user")装饰的方法应该写在它之前。

全局路由前缀

除了上面这些装饰器可以设置路由外, 我们还可以设置全局路由前缀, 比如给所以路由都加上/api前缀。此时需要修改main.ts
此时之前的路由,都要变更为:
到此我们认识了ControllerServiceModule、路由以及一些常用的装饰器, 那接下来就实战一下,我们以开发文章(Post)模块作为案例, 实现文章简单的CRUD,带大家熟悉一下这个过程。

编写代码

写代码之前首先介绍几个nest-cli提供的几个有用的命令:
  • 创建模块
nest g mo posts 创建一个 posts模块,文件目录不写,默认创建和文件名一样的posts目录,在posts目录下创建一个posts.module.ts
执行完命令后,我们还可以发现同时在根模块app.module.ts中引入PostsModule这个模块,也在@Model装饰器的inports中引入了PostsModule
  • 创建控制器
nest g co posts
此时创建了一个posts控制器,命名为posts.controller.ts以及一个该控制器的单元测试文件.
执行完命令, 文件posts.module.ts中会自动引入PostsController,并且在@Module装饰器的controllers中注入。
  • 创建服务类
nest g service posts
创建app.service.ts文件,并且在app.module.ts文件下,@Module装饰器的providers中注入注入。
其实nest-cli提供的创建命令还有很多, 比如创建过滤器、拦截器和中间件等,由于这里暂时用不到,就不过多的介绍,后面章节用到了再介绍。
注意创建顺序: 先创建Module, 再创建Controller和Service, 这样创建出来的文件在Module中自动注册,反之,后创建Module, Controller和Service,会被注册到外层的app.module.ts
看一下现在的目录结构:
notion image

连接SqlLite

路由生效了,既然是后端项目,必须得用上数据库,不然和写静态页面自己玩没什么区别。
数据库我选择的是Mysql,毕竟实际项目中大多数还是选择它的。因为文章属于从零教程, 所以会包含数据库的安装、连接、使用以及使用过程遇到的坑,如果你是有经验的老手,可以跳过这部分。

1. 配置Prisma

将 Prisma CLI 作为开发依赖项安装在项目中:
最后,使用 Prisma CLI 的命令设置 Prisma:init
这会创建一个包含 Prisma 架构文件的新目录,并将 SQLite 配置为数据库。现在,你已准备好对数据进行建模并使用一些表创建数据库。prisma
 
Prisma 架构提供了一种直观的数据建模方法。将以下模型添加到文件中:schema.prisma
Prisma 架构中的模型有两个主要用途:
  • 表示基础数据库中的表
  • 作为生成的Prisma Client API的基础
 
此时,您有一个 Prisma 架构,但还没有数据库。在终端中运行以下命令以创建 SQLite 数据库和模型表示的 和 表:UserPost
此命令执行了两项操作:
  1. 它会在目录中为此迁移创建新的 SQL 迁移文件。prisma/migrations
  1. 它针对数据库运行 SQL 迁移文件。
由于 SQLite 数据库文件以前不存在,因此该命令还在目录中创建了它,其名称通过文件中的环境变量定义。prismadev.db.env
恭喜,您现在已准备好数据库和表。让我们去学习如何发送一些查询来读取和写入数据!
 
要将查询发送到数据库,您需要一个 TypeScript 文件来执行 Prisma 客户端查询。为此创建一个新文件:script.ts
此代码包含在脚本末尾调用的函数。它还实例化哪个表示数据库的查询接口。mainPrismaClient
 
让我们从一个小查询开始,在数据库中创建新记录,并将生成的对象记录到控制台。将以下代码添加到文件中:Userscript.ts
您无需复制代码,而是可以在编辑器中键入代码以体验 Prisma 客户端提供的自动完成功能。您还可以通过按键盘上的 + 键主动调用自动完成。CTRLSPACE
接下来,使用以下命令执行脚本:
干得好,您刚刚使用 Prisma 客户端创建了您的第一个数据库记录!🎉

3. 连接Prisma到NestJS

4. 使用Prisma

接口格式统一

一般开发中是不会根据HTTP状态码来判断接口成功与失败的, 而是会根据请求返回的数据,里面加上code字段
首先定义返回的json格式:
请求失败时返回:

拦截错误请求

首先使用命令创建一个过滤器:
过滤器代码实现:
最后需要在main.ts中全局注册
这样对请求错误就可以统一的返回了,返回请求错误只需要抛出异常即可,比如之前的:
接下来对请求成功返回的格式进行统一的处理,可以用Nest.js的拦截器来实现。

拦截成功的返回数据

首先使用命令创建一个拦截器:
拦截器代码实现:
最后和过滤器一样,在main.ts中全局注册:
过滤器和拦截器实现都是三部曲:创建 > 实现 > 注册,还是很简单的。
现在我们再试试接口,看看返回的数据格式是不是规范了?
notion image
一名合格的前端,你对我说:"这是接口地址xxx, 用postman执行一下就能看到返回结果",这完全就是在挑衅, 鬼知道你每个字段什么意思,每个接口需要传什么参数,哪些参数必传,哪些可选....
反正要是我拿到这样的接口,肯定会喷~

配置接口文档Swagger

所以我们接下来就讲一下怎么写接口文档,既高效又实用。我这里用swagger,用它的原因一方面是 Nest.js提供了专用的模块来使用它,其次可以精确的展示每个字段意义,只要注解写的到位!
说心里话, 使用体验一般般,只能说还行
首先安装一下:
我这里安装的版本是:5.1.4, 和4.x.x版本相比有些API的变动。
接下来需要在main.ts中设置Swagger文档信息:
配置完成,我们就可以访问:http://localhost:9080/docs,此时就能看到Swagger生成的文档:
我们写的路由都展示出来了,但是我们就这么看,找需要的接口也太难了,而且这些接口仍然没有任何注释,还是看不懂啊~

接口标签

我们可以根据Controller来分类, 只要添加@ApiTags就可以
posts.controller.tsapp.controller.ts 都分别加上分类标签,刷新Swagger文档,看到的效果是这样的:
notion image

接口说明

进一步优化文档, 给每一个接口添加说明文字, 让使用的人直观的看到每个接口的含义,不要让使用的人去猜。同样在Controller中, 在每一个路由的前面使用@ApiOperation装饰器:
现在我们对每一个接口都写上了说明,再来看看接口文档展现:
notion image

接口传参

最后我们要处理的就是接口参数说明, Swagger的优势之一就是,只要注解到位,可以精确展示每个字段的意义,我们想要对每个传入的参数进行说明。
这里需要先插入一段关于DTO的解释, 因为后面参数说明会用到:
数据传输对象(DTO)(Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输目标往往是数据访问对象从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)。
这一段是官方解释, 看不懂没关系,可以理解成,DTO 本身更像是一个指南, 在使用API时,方便我们了解请求期望的数据类型以及返回的数据对象。先使用一下,可能更方便理解。
posts目录下创建一个dto文件夹,再创建一个create-post.dot.ts文件:
然后在Controller中对创建文章是传入的参数进行类型说明:
这里提出两个问题:
  1. 为什么不使用 interface 而要使用 class 来声明 CreatePostDto
  1. 为什么不直接用之前定义的实体类型PostsEntiry,而是又定义一个 CreatePostDto
如果你想到这里,很好,说明你一直在思考,下面我们一边继续完善Swagger接口文档,一般解释这两点。
对于第一个问题,我们都知道Typescript接口在编译过程中是被删除的,其次后面我们要给参数加说明,使用Swagger的装饰器,interface也是无法实现的,比如:
@ApiPropertyOptional装饰可选参数,继续看开一下API文档的UI:
notion image
对于上面提到的第二个问题,为什么不直接使用实体类型PostsEntiry,而是又定义一个 CreatePostDto,因为HTTP请求传参和返回的内容可以采用和数据库中保存的内容不同的格式,所以将它们分开可以随着时间的推移及业务变更带来更大的灵活性,这里涉及到单一设计的原则,因为每一个类应该处理一件事,最好只处理一件事。
现在就可以从API文档上直观的看到每个传参的含义、类型以及是否必传。到这一步并没有完, 虽然以及告诉别人怎么传, 但是一不小心传错了呢, 比如上面作者字段没传,会发生什么呢?
notion image
接口直接报500了, 因为我们实体定义的author字段不能为空的,所有在写入数据时报错了。这样体验非常不好, 很可能前端就怀疑我们接口写错了,所有我们应该对异常进行一定的处理。

数据验证

怎么实现呢?首先想到的是在业务中去写一堆的if-elese判断用户的传参,一想到一堆的判断, 这绝对不是明智之举,所有我去查了Nest.js中数据验证,发现Nest.js中的管道就是专门用来做数据转换的,我们看一下它的定义:
管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。
管道有两个类型:
  • 转换:管道将输入数据转换为所需的数据输出
  • 验证:对输入数据进行验证,如果验证成功继续传递; 验证失败则抛出异常;
notion image
管道在异常区域内运行。这意味着当抛出异常时,它们由核心异常处理程序和应用于当前上下文的 异常过滤器 处理。当在 Pipe 中发生异常,controller 不会继续执行任何方法。
什么意思呢, 通俗来讲就是,对请求接口的入参进行验证和转换的前置操作,验证好了我才会将内容给到路由对应的方法中去,失败了就进入异常过滤器中。
Nest.js自带了三个开箱即用的管道:ValidationPipeParseIntPipeParseUUIDPipe, 其中ValidationPipe 配合class-validator就可以完美的实现我们想要的效果(对参数类型进行验证,验证失败抛出异常)。
管道验证操作通常用在dto这种传输层的文件中,用作验证操作。首先我们安装两个需要的依赖包:class-transformerclass-validator
然后在create-post.dto.ts文件中添加验证, 完善错误信息提示:
入门阶段,我们使用的数据比较简单,上面只编写了一些常用的验证,class-validator还提供了很多的验证方法, 大家感兴趣可以自己看官方文档.
最后我们还有一个重要的步骤, 就是在main.ts中全局注册一下管道ValidationPipe
此时我们在发送一个创建文章请求,不带author参数, 返回数据有很清晰了:
notion image
通过上边的学习,可以知道DTO本身是不存在任何验证功能, 但是我们可以借助class-validator来让DTO可以验证数据
 

参考链接

  1. 学完这篇 Nest.js 实战,还没入门的来锤我!(长文预警) - 掘金 (juejin.cn)
 
上一篇
NestJS 学习 —— 基本概念
下一篇
NestJS 学习 —— IoC & TS装饰器

评论
Loading...