前言
nestjs
为后台的一门技术框架,是前端转向全栈非常好的一个切入点,其完美支持 typescript
,也可以使用JavaScript
,且nestjs
具备渐进增强的能力,可以边开发边学习,且使用入门简单,几天下来就可以连学带写一个小项目,可以说非常受欢迎,这篇文章就介绍他基础入门吧
nestjs、nestjs-中文文档(非官网具备滞后、误译特性,访问快)
配置项目
js
// 全局安装Nest,理解为安装nest环境
npm i -g @nestjs/cli
// 使用nest命令创建项目,后面基本就靠他了
nest new project-name
我们可以选择自己爱好的包管理器(个人比较喜欢 yarn)
就这样一个项目就创建完成了
我们可以看到上面默认创建了一个目录,主要有下面几个(实际上更多),后续用到简介
main
:main 函数,不多说,程序入口
conroller
:程序路由,也就是接口来源地,一般用来分发功能,具体实现交个一个至多个 service 编写
service
:提供服务的文件 service 也叫 provider,我们的业务逻辑基本上都在这里写,无论是对数据库的增删改查,还是业务逻辑的编写都在这里(当然一部分公共库还是要提取的,跟其他开发一样)
module
:模块管理器,我们的应用由一个根模块和多个子模块组成,首先是根模块,然后是子模块,当然特别小的程序可能只有一个跟模块,这里比较推荐根据功能分割成多个子模块,日后方便管理
扩展: 当然后面还有什么 entity(数据库映射表)、dto(接口参数规范)、guard(鉴权)等,后面会介绍到的(也许当前文章没有)
扩展2: IOC机制,在 controller、service 中引入内容相应功能时,可以参考 module 中注入的内容,不需要 new,直接使用即可
nest指令
我们配置完成后,就可以使用 nest 指令了,如果不记得也没事,很简单,调用 -help 即可
js
//查看有哪些命令
nest -help
个人感觉,开始时用的最多的就是 res、s、gu了,当然也看个人开发习惯
下面创建一个 user 模块吧,我们使用 res 命令
js
//创建一个 user resource 仓库
nest g res user
其中里面会出现一堆 .spec.ts
,这是做单元测试用了,自学阶段嫌乱,可以直接删了(也可以根据情况写自测代码)
路由
指的就是 controller
文件在做的事情,我们的功能通常会被分成多个模块,每个模块都会有一个自己的controller
,他可以帮我们定义接口的名字和位置
作为路由,主要是分发功能使用,实际业务实现逻辑要分发到其他 service
中编写,否则 controller
会很肿胀,如果不按规范,项目比较大,合作开发时,无论是自己看以前代码,还比别人看自己代码都会很麻烦
从这里开始就会用到各种各样的装饰器
,如果介绍的不够多,可以的到这里参考
controller 头部介绍
js
@ApiTags('user') //设置swagger文档用的,标记路由所属模块,用于区分api,后面也会介绍
@Controller('user') //设置该模块根路由位置,默认会有
export class UserController {
constructor(private readonly userService: UserService) { }
}
网络请求
常见的网络请求有 get、post、put、patch、delete、headers
等,也就 rest 风格多一点,现在业务比较杂,很多功能并不是那么单一,因此一般就 get + post
就全部拿下了(业务简单,公司有要求,可以改成 rest 风格,其实都不差太多)
这里通过ts装饰
来标记我们的请求类型,例如:
js
//声明接口的请求类型,
@Get()
@Post()
@Delete()
@Put()
//在模块根路由上增加一个get类型接口,通过该模块跟路由访问
//这样编写一个模块只能调用一个 get 方法
//例如:...api/user?id=123
@Get()
getUserInfo(...) {
return ...
}
//命名追加到url路由上,这样可以区分不同的get类型
//例如:...api/user/getUserInfo?id=123
@Get('getUserInfo') //给接口追加子路由
getUserInfo(...) {
return ...
}
其他的不多说,下面会稍微详细一点介绍一下 get
和 post
get请求值 params类型参数**
这种请求以前使用的比较多,现在也是比较少用的,基本都改成 query 类型参数了,但是也要了解
params
类型参数会直接将参数值
拼到路由
上传给服务器,如下所示
js
//使用params类型,根据id查询
//.../api/id_value //id的值会传递在url上
@Get(':id')
getUserFriends(
@Param('id') id: string //声明一个 param 类型 id,类型为 string
) {
return {
name: 'friend1',
age: 20
}
}
get请求query类型参数
现在 get
请求基本上都是使用 query
的形式传递,这样不会污染我们的路由,参数位置一目了然
其中 query
类型如下所示,仍然是 get 特色,参数内容都在 url 上,会暴露参数,接口容易被利用,适合一些公共无安全问题的读取信息接口,例如:排行榜、说明书等
js
// 使用query类型
// .../api?id=value&...
@Get('getUserInfo') //命名追加到url路径上
getUserInfo(
@Query('id') id: string //声明一个 query 类型 id,类型为string
) {
return {
name: '哈哈',
age: 22,
}
}
post请求与body
post
请求是最受大家喜欢的接口了,url 信息暴露问题解决了,参数都放到了 body(data)
中,因此外部看不到,一般用来做创建、更新、删除等操作
js
@Post('updateUserInfo') //声明post接口类型,路由为updateUserInfo
@APIResponse(UserDto)
updateUserInfo(
@Headers() headers: any, //可以获取headers中的内容,例如版本号平台
@Body() userInfo: UserDto //定义body体类型dto,规范参数类型
) {
return {
message: 'ok'
}
}
参数dto与pile校验
dto
(Data Transfer Object),其实这里就是用来定义参数,规范文档和使用的一个类型罢了,除了规范参数使用,也可以用来校验和生成参数文档
pile
就是内容校验通道,我们可以通过引入 class-validator
js
//使用 yarn 引入 class-validator
yarn add class-validator
如下所示,使用ts装饰
配置一些校验器即可,类型不对,会在 message
以一个数组的方式返回所有出现的参数错误提示,有很多相关判断,可以点进装饰器,看目录就能了解很多装饰器
js
export class UserDto {
//api属性备注,必填
@IsNotEmpty({ message: '名称不能为空' }) //可以返回指定message,返回为数组
readonly name: string
//可选参数
@IsNotEmpty() //返回默认 message,且为字符串数组,毕竟可能存在多个为空的
readonly age: number
readonly mobile: string
//自定义校验类型
@Validate((val: number) => val >=0 && val <= 1)
readonly sex: number
@IsBoolean()
isMarry: boolean
}
除了上面还要在 main
函数加入下面代码,将其设为全局
js
app.useGlobalPipes(new ValidationPipe());
如果不填写,会报如下错误(当然可以通过拦截器,统一一下最后的错误处理,避免返回数据格式和自己定下的类型不一致,后面会讲)
当然 get
的 query
也可以设置,只不过没有那么便利,或者那么校验了,只能使用默认的几种,例如:下面的 ParseIntPipe 判断是否是 int 类型,还可以 float、bool、array 等
js
@Get('getUserInfo') //命名追加到url路径上
getUserInfo(
@Query('id', new ParseIntPipe()) //如果不是int类型会报错
// @Query('id', new ParseIntPipe({
// errorHttpStatusCode: HttpStatus.NOT_FOUND, //也可以指定httpcode类型,但一般都是用自己的错误类型
// }))
id: number //声明一个 query 类型 id,类型为number
)
一般存在校验的,推荐 dto + pile校验
这种,像 get 这种,一般都是参数极为简单或者没参数,可以直接使用上面的,或者直接手动抛出异常即可
post上传文件form/data
上传 form-data类型数据时, 客户端需要指定 content type 为 multipart/form-data(有些固定的调用不需要)
js
//上传单个文件
@Post('file')
@UseInterceptors(FileInterceptor('file'))
uploadFiles(
@UploadedFile() file: Express.Multer.File,
) {
console.log(files);
}
//上传多个文件
@Post('file')
@UseInterceptors(FilesInterceptor('file'))
uploadFiles(
@UploadedFiles() files: Array<Express.Multer.File>,
) {
console.log(files);
}
//上传带其他参数的文件
@Post('file')
@UseInterceptors(AnyFilesInterceptor({
dest: 'uploads/',
}))
uploadFile(
@Body() objDto: ObjDto,
@UploadedFiles() files: Array<Express.Multer.File>,
) {
console.log(files);
console.log(objDto)
}
typeorm连接mysql数据库
typeorm
是数据库的一个映射工具,会将我们创建的数据库类型、数据库操作映射
到 mysql
上,是一个封装后的mysql
简化操作工具,减少了直接写 sql语句
操作数据库中的很多麻烦
因此我们需要进行如下步骤,才可以完成数据库的成功连接:
安装配置mysql并打开数据库服务
-> 连接并创建数据库database
-> 配置nest的typeorm映射关系
-> 开始我们的数据库操作
分布式、微服务简介
经过上面步骤,在配置的过程,可能也会颠覆扩展一些小白前端的认知,前端和移动端基本上就是直接创建数据库然后操作即可,基本可以理解都在一个设备上
后台不太一样,其很有可能是分布式的,即:后台连接的数据库可能部署在后台本地,也可能部署在远端服务器,那样后台就和我们前端一样,主要就负责写业务了,需要读写数据时,除了本地服务器,需要连接远端一台或者多台服务器读写数据,这就是后台常见的分布式部分概念了,另外,当我们的项目功能比较复杂,可能会吧一个项目的多个模块,分割成多个小项目独立运行,他们共同访问属于自己的数据库服务器或者公共数据库服务器,分割多个子项目独立运行作用整体,其就是微服务架构
,错综复杂的整个系统就是分布式系统
这下相信也可以理解,为何一个后台项目可以使用多套技术栈来运行了吧,那就是将业务分割成多个服务,整体形成一个庞大的分布式系统,项目越大内容越杂,整个分布式系统也会看着越繁琐
安装mysql(mac端)
mysql下载地址,我们到这里直接下载 dmg 即可,要是服务器一般为远端 linux, 直接进入服务器,按照别人的步骤来下载配置即可
安装完毕后,在系统偏好找打 mysql,然后启动即可,忘记密码也可以点进去配置,比以前方便太多了
ps
:我自动自动后,基本都是开机自启,基本不用管了(甚至时间久了都会忘了流程,毕竟太简单了😂)
连接数据库
到这里数据库服务我们开启了,但我们还没创建数据库,因此需要创建数据库(nest只会创建表和读写,不能创建数据库)
创建前我们需要下载一些工具来操作和查看数据库,可以已使用 appstore
上面的一些收费
的软件,其看起来比较舒服,但需要马内,也可以使用vscode插件
等,个人倾向 vscode插件
,毕竟免费
,丑点无所谓
打开 vscode
搜索 database client
,然后下载,下载后我们打开,会出现这个界面,直接输入我们的密码和数据库类型名称即可,数据库填写 mysql
,不要填写我们自定义的 database 名字,如下所示填写完成后,点,连接成功
到这里我们还需要创建我们的数据库 database,因此需要命令,我们新建一个,会出现下面命令,我们在 CREATE DATABASE
后面加上我们自定义的 database 名字即可,这里叫 nest_demo,当然也可以创建连接多个数据库
到这里就完成了,后面只需要使用 typeorm 库连接我们的数据库,并配置好数据库字段映射操作即可,配置完成后,下次运行便会出现我们的表了(虾米那会介绍怎么连接和映射)
typeorm配置与环境变量
话不多说,先安装下面是三个库 typeorm、mysql
js
yarn add @nestjs/typeorm typeorm mysql2
然后配置 app.module
,可以发现使用了 TypeOrmModule.forRoot
,在里面可以直接写入自己的地址、端口号、密码等,这里面没有直接代码写死,是因为使用环境变量的方式,方便后期部署时随时更改地址
js
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: envConfig.host,
port: Number(envConfig.port),
username: envConfig.username,
password: envConfig.password,
database: envConfig.database,
synchronize: true, //自动同步创建数据库表
retryDelay: 500,
retryAttempts: 10,
autoLoadEntities: true, //自动查找entity实体
})
],
controllers: [AppController],
providers: [AppService],
})
创建了一个 .env
的环境变量文件,在里面填写我们用到的一些相关变量
创建一个 config
文件,然后用于获取转化我们的环境变量,方便使用即可(注意:本地服务可能和数据库不在一个,那是服务的 host、port 自己要区分开)
js
import * as dotenv from 'dotenv';
class ConfigEnv {
secret: string;
host: string;
port: string;
//mysql
username: string;
password: string;
database: string;
constructor(envConfig: any) {
this.secret = envConfig.APP_SECRET
this.host = envConfig.DB_HOST
this.port = envConfig.DB_PORT
this.username = envConfig.DB_USER
this.password = envConfig.DB_PASSWORD
this.database = envConfig.DB_DATABASE
}
}
const envConfig = new ConfigEnv(dotenv.config().parsed)
export { envConfig };
这样基础配置好了,但还没完事,我们还没有建立数据库映射,因此还无法自动新建数据库
crud映射
设置数据库映射 entity 表,这里设置完毕后,会自动映射出数据库 table 表
还记得我们前面 nest g res user
创建的用户模块么,里面有一个 user.entity.ts 文件,我们在里面映射我们的表即可
如下所示,我们建立了一个简单的用户表,方便介绍
js
@Entity() //默认带的 entity
export class User {
//作为主键且创建时自动生成,默认自增
@PrimaryGeneratedColumn()
id: number
//默认数据库的列,会根据 ts 类型,自动创建自定类型,默认字符串 255 byte,也就是255个unicode字符
@Column()
name: string
//可以设置唯一值,参数可以点进去看详情
@Column({unique: true})
wxId: string
//设置默认值
@Column({default: null})
age: number
//设置枚举,实际推荐数字 + 文档即可,方便又实惠
@Column('simple-enum', {enum: ['man', 'woman', 'unknow']})
sex: string
@Column({default: null}) //默认最大字符串255字节,能储存255个unicode字符
mobile: string
@Column({ select: false, length: 30}) //查询时隐藏此列,可以设置长度30个字节
password: string
//默认都是可变字节,如果设置最大长度比较小,但内容比较大,也能写入,但是效率可能会变低
//默认最大字节数比较大,65535为text,另一个更大,也可以根据自行设置大小
// @Column('mediumtext', {default: null})
@Column('text', {default: null})
desc: string
//伪/软删除,用户误操作可以恢复,对于重要/敏感信息,不能真删除
@Column({ select: false}) //查询时隐藏此列
isDelete: boolean
@VersionColumn() //自动记录内容更新次数,某些计次场景会用到
version: number
//下面是创建内容自动生成,和更新时自动更新的时间戳,分别代表该条记录创建时间和上次更新时间
@CreateDateColumn({ type: 'timestamp' })
createTime: Date
@UpdateDateColumn({ type: 'timestamp' })
updateTime: Date
}
其他还有需要的功能,自己点进装饰器看就可以了,我也是功能不够用时,点进去找的🤣
service服务
我们的 controller 主要是充当路由,而数据提取、业务逻辑等都在 service 中编写,因此其很重重要,有时候根据业务和功能,一个小模块会分成好多 service
编写 service 时,如果出现问题错误返回给外面,直接 throw 即可,正常我们一般会包装一层固定结构返回,后面介绍swagger时一起介绍
js
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(Account)
private accountRepository: Repository<Account>,
) {}
getUserInfo(id: number) {
//nest的查询语句,这句意思和 findOne 一样,根据表当中的某个字段获取一个,可以点进去查看
//复杂的查询逻辑,还是需要对数据库多学习了解的
return this.userRepository.findOneBy({id})
}
}
全局拦截器
全局拦截器的文件可以通过 nest
指令,也可以直接直接创建编写,下面直接编写即可,为了方便可以使用
ps1 :仅个人感觉
来说,两个都不是很好用,尤其是成功后的拦截器,其有点鸡肋(可能是我用的方式不对哈),因为我们只能修改里面的 data 数据结构,外层的还得按照 http 协议的走,失败的倒是还不错,我们可以让用户按照我们的显示,不过客户端还没到服务器阶段产生的网络错误,还得按照他自己的逻辑走,并且使用时我们要占用一个 http 状态码,并且一些是我们请求成功了,但是不符合业务要求的错误,抛出异常会到这里,这时状态码用着有点不太舒服,最好在成功里面,因此不使用全局,(自定义一个类,返回最好)
ps2:后面单独介绍文档 swagger 时,会一起讲解一下个人思路
错误过滤拦截
创建一个 http-exception.filter.ts
文件
js
import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse(); // 获取请求上下文中的 response对象
const status = exception.getStatus(); // 获取异常状态码
let message: string;
let code: number;
if (status === 401) {
code = status;
message = '未授权';
}else {
code = -1;
// message = exception.message
message = '网络请求失败';
}
// 设置返回的状态码, 请求头,发送错误信息
response.status(status);
// response.header('Content-Type', 'application/json; charset=utf-8');
response.send({
msg: message,
code,
});
}
}
成功格式拦截
创建一个 transform.interceptor
文件
js
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return {
data,
code: 200,
msg: '请求成功',
};
}),
);
}
}
配置swagger
配置前的口嗨
这一步也是很重要的一个步骤,可以说不会写、不写文档的后台不是一个合格的后台
ps :前端平生最恨两种后台,一个是文档全靠嘴,接口说改就改,最后找前端要文档
,另一个就是一运行就报错,对错全靠前端测
因此,写文档是必要的,接口写完可以先拿 postman
等自测一下吧,不谈功能对不对,运行先不报错500吧😂
配置文档
安装swagger
先使用 npm、yarn
导入 swagge
相关
js
yarn add @nestjs/swagger swagger-ui-express
配置main函数
然后再 main
函数开启文档,当我们项目运行的时候,文档就能看到了
js
const options = new DocumentBuilder()
.setTitle('nest demo api')
.setDescription('This is nest demo api')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api-docs', app, document);
//这个地址本地看得话就是 http://localhost:3000/api-docs了
main 函数配置的就是,swagger顶部那些,如下所示,下面的也会介绍
配置路由、dto文档
配置模块名称 @ApiTags
,也就是给一个模块添加一个大标题索引,方便快速区分api功能的
js
@ApiTags('user')
@Controller('user')
export class UserController {
......
}
配置接口路由备注 @ApiOperation
,可以设置单个接口备注
,上图就可以看出来
js
@ApiOperation({
summary: '修改用户信息2',
})
@Post('updateUserInfo')
updateUserInfo(//可以获取headers中的内容,例如版本号平台
@Body() userInfo: UserDto
) {
......
}
前面给 @Body
后续前面设置了 dto
,dto
也可以配置上传参数的标签,以便于用户设置,再加上以前的参数验证pipe,如下所示
下面 description
参数描述, example
为参数案例,@ApiProperty
表示必填,@ApiPropertyOptional
表示可选属性
js
export class UserDto {
//api属性备注,必填
@ApiProperty({description: '名字', example: '迪丽热巴'})
//设置了 IsNotEmpty 就是必填属性了,文档也会根据该验证来显示是否必填
@IsNotEmpty({ message: 'name不能为空' })//可以返回指定message,返回为数组
// @IsNotEmpty()//返回默认message,默认为为原字段的英文提示
readonly name: string
//可选参数
@ApiPropertyOptional({description: '年龄', example: 20})
readonly age: number
@ApiPropertyOptional({description: '手机号', example: '133****3333'})
readonly mobile: string
@ApiPropertyOptional({description: '性别 1男 2女 0未知', example: 1})
@Validate((val: number) => !val || (val > 0 && val <= 1))
readonly sex: number
@ApiPropertyOptional({description: '是否已婚', example: false})
@IsNotEmpty()
marry: number
}
body
传参如下所示,可以点击 schema
查看必填项
后面还会增加 swagger 更加详细的内容,这篇就介绍到这里了(文档很重要,单独搞一篇出来,保证能做出来一个相对比较好的 http 文档)
设置api路由前缀
有时为了使接口api更加清晰化,或预留位置等情况,我们开发时,会给我们的项目添加一个全局路由
js
app.setGlobalPrefix('api');
这样接口的基础地址就会变成 http://localhost:3000/api
文档还是原来那个不受影响 /api-docs
设置跨域支持
前端开发不可避免的会遇到跨域问题,如果是测试阶段还需要使用代理,为了方便调试后端可以关闭跨域,如下所示
js
//设置跨域支持
app.enableCors();
最后
本篇文章主要是入门,篇幅太大就比较难读了,到这里基本上就能开始写了,不会的也知道从哪里查了,后续也会略微完善一点
测试案例demo ------ 最后附上是因为里面还有后面的其他功能,相对比较杂,有些功能会有不小改动,直接拿 demo 学习对一些人会有影响
ps
:我们在开发中,可能会给前端写接口,移动端,尤其是对于移动端来说,更新较慢,因此当我们上线应用时,更改原有功能时,有较大改动时(数据结构发生改变),可以新增接口或者添加版本判断等,不要轻易直接动原接口,否则可能会导致线上应用显示异常、崩溃等,毕竟短时间内用户不一定会升级应用,因此,一些必要的措施是要做的,此外,我们应用可以加上一个版本统计功能,这样等老接口都退休无人访问的时候,就可以真的退休了,另外一定要注释好了
祝大家学习愉快!