nestjs-基础入门

前言

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

nestjsnestjs-中文文档(非官网具备滞后、误译特性,访问快)

配置项目

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 ...
}

其他的不多说,下面会稍微详细一点介绍一下 getpost

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());

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

当然 getquery 也可以设置,只不过没有那么便利,或者那么校验了,只能使用默认的几种,例如:下面的 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 后续前面设置了 dtodto也可以配置上传参数的标签,以便于用户设置,再加上以前的参数验证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:我们在开发中,可能会给前端写接口,移动端,尤其是对于移动端来说,更新较慢,因此当我们上线应用时,更改原有功能时,有较大改动时(数据结构发生改变),可以新增接口或者添加版本判断等,不要轻易直接动原接口,否则可能会导致线上应用显示异常、崩溃等,毕竟短时间内用户不一定会升级应用,因此,一些必要的措施是要做的,此外,我们应用可以加上一个版本统计功能,这样等老接口都退休无人访问的时候,就可以真的退休了,另外一定要注释好了

祝大家学习愉快!

相关推荐
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(二十六):icon 组件封装
前端·vue.js·typescript
PBitW8 小时前
为什么vite中TS报错,可以继续运行?Webpack不行?💡
前端·typescript
敖行客 Allthinker10 小时前
前端调试实战:DevTools调试工具 + 二分定位Bug法全总结
前端·typescript·chrome devtools
不瘦80斤不改名10 小时前
HalfCart:基于LBS的1\.5km本地拼单系统全栈实践与踩坑复盘
python·docker·typescript·pycharm
濮水大叔1 天前
在大型 Vue 项目中,如何有效管理复杂的状态?“散装版” vs “套装版”
前端·vue.js·typescript
谷哥的小弟2 天前
TypeScript数组类型
javascript·typescript
退休倒计时2 天前
【每日一题】LeetCode 199. 二叉树的右视图 TypeScript
算法·leetcode·typescript
前端之虎陈随易3 天前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·vue.js·人工智能·typescript·node.js
后台开发者Ethan3 天前
类型分类、联合类型、交叉类型
typescript
鹰影473 天前
一款AI笔记助手和远程同步的markdown笔记idea-note
人工智能·笔记·rust·typescript·react