龙空技术网

nestjs-基础入门

Java技术栈论坛 79

前言:

现在姐妹们对“js获取transform”大体比较关注,朋友们都需要学习一些“js获取transform”的相关知识。那么小编在网上汇集了一些对于“js获取transform””的相关知识,希望同学们能喜欢,我们一起来学习一下吧!

前言

nestjs 为后台的一门技术框架,是前端转向全栈非常好的一个切入点,其完美支持 typescript,也可以使用JavaScript,且nestjs具备渐进增强的能力,可以边开发边学习,且使用入门简单,几天下来就可以连学带写一个小项目,可以说非常受欢迎,这篇文章就介绍他基础入门吧

配置项目

 // 全局安装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 即可

//查看有哪些命令nest -help

个人感觉,开始时用的最多的就是 res、s、gu了,当然也看个人开发习惯

下面创建一个 user 模块吧,我们使用 res 命令

//创建一个 user resource 仓库nest g res user

其中里面会出现一堆 .spec.ts,这是做单元测试用了,自学阶段嫌乱,可以直接删了(也可以根据情况写自测代码)

路由

指的就是 controller 文件在做的事情,我们的功能通常会被分成多个模块,每个模块都会有一个自己的controller,他可以帮我们定义接口的名字和位置

作为路由,主要是分发功能使用,实际业务实现逻辑要分发到其他 service 中编写,否则 controller 会很肿胀,如果不按规范,项目比较大,合作开发时,无论是自己看以前代码,还比别人看自己代码都会很麻烦

从这里开始就会用到各种各样的装饰器,如果介绍的不够多,可以的到这里参考

controller 头部介绍

@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装饰来标记我们的请求类型,例如:

//声明接口的请求类型,@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类型参数会直接将参数值 拼到路由上传给服务器,如下所示

//使用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 上,会暴露参数,接口容易被利用,适合一些公共无安全问题的读取信息接口,例如:排行榜、说明书等

// 使用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)中,因此外部看不到,一般用来做创建、更新、删除等操作

@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

//使用 yarn 引入 class-validator yarn add class-validator

如下所示,使用ts装饰配置一些校验器即可,类型不对,会在 message 以一个数组的方式返回所有出现的参数错误提示,有很多相关判断,可以点进装饰器,看目录就能了解很多装饰器

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 函数加入下面代码,将其设为全局

app.useGlobalPipes(new ValidationPipe());

如果不填写,会报如下错误(当然可以通过拦截器,统一一下最后的错误处理,避免返回数据格式和自己定下的类型不一致,后面会讲)

当然 get 的 query 也可以设置,只不过没有那么便利,或者那么校验了,只能使用默认的几种,例如:下面的 ParseIntPipe 判断是否是 int 类型,还可以 float、bool、array 等

@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(有些固定的调用不需要)

//上传单个文件@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

yarn add @nestjs/typeorm typeorm mysql2

然后配置 app.module,可以发现使用了 TypeOrmModule.forRoot,在里面可以直接写入自己的地址、端口号、密码等,这里面没有直接代码写死,是因为使用环境变量的方式,方便后期部署时随时更改地址

@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 自己要区分开)

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 文件,我们在里面映射我们的表即可

如下所示,我们建立了一个简单的用户表,方便介绍

@Entity()  //默认带的 entityexport 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时一起介绍

@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 文件

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 文件

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相关

yarn add @nestjs/swagger swagger-ui-express
配置main函数

然后再 main函数开启文档,当我们项目运行的时候,文档就能看到了

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);//这个地址本地看得话就是 了

main 函数配置的就是,swagger顶部那些,如下所示,下面的也会介绍

配置路由、dto文档

配置模块名称 @ApiTags,也就是给一个模块添加一个大标题索引,方便快速区分api功能的

@ApiTags('user')@Controller('user')export class UserController {    ......}

配置接口路由备注 @ApiOperation,可以设置单个接口备注,上图就可以看出来

@ApiOperation({    summary: '修改用户信息2',})@Post('updateUserInfo')updateUserInfo(//可以获取headers中的内容,例如版本号平台    @Body() userInfo: UserDto) {   ......}

前面给 @Body 后续前面设置了 dto,dto也可以配置上传参数的标签,以便于用户设置,再加上以前的参数验证pipe,如下所示

下面 description参数描述, example 为参数案例,@ApiProperty表示必填,@ApiPropertyOptional表示可选属性

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更加清晰化,或预留位置等情况,我们开发时,会给我们的项目添加一个全局路由

app.setGlobalPrefix('api');这样接口的基础地址就会变成 文档还是原来那个不受影响 /api-docs
设置跨域支持

前端开发不可避免的会遇到跨域问题,如果是测试阶段还需要使用代理,为了方便调试后端可以关闭跨域,如下所示

//设置跨域支持app.enableCors();
最后

本篇文章主要是入门,篇幅太大就比较难读了,到这里基本上就能开始写了,不会的也知道从哪里查了,后续也会略微完善一点

测试案例demo ------ 最后附上是因为里面还有后面的其他功能,相对比较杂,有些功能会有不小改动,直接拿 demo 学习对一些人会有影响

ps:我们在开发中,可能会给前端写接口,移动端,尤其是对于移动端来说,更新较慢,因此当我们上线应用时,更改原有功能时,有较大改动时(数据结构发生改变),可以新增接口或者添加版本判断等,不要轻易直接动原接口,否则可能会导致线上应用显示异常、崩溃等,毕竟短时间内用户不一定会升级应用。因此,一些必要的措施是要做的,此外,我们应用可以加上一个版本统计功能,这样等老接口都退休无人访问的时候,就可以真的退休了,另外一定要注释好了

祝大家学习愉快!

标签: #js获取transform #js变量命名bool