likes
comments
collection
share

nestjs-基础入门

作者站长头像
站长
· 阅读数 15

前言

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

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

配置项目

 // 全局安装Nest,理解为安装nest环境
npm i -g @nestjs/cli

// 使用nest命令创建项目,后面基本就靠他了
nest new project-name 

我们可以选择自己爱好的包管理器(个人比较喜欢 yarn)

nestjs-基础入门

就这样一个项目就创建完成了

nestjs-基础入门

我们可以看到上面默认创建了一个目录,主要有下面几个(实际上更多),后续用到简介

main:main 函数,不多说,程序入口

conroller:程序路由,也就是接口来源地,一般用来分发功能,具体实现交个一个至多个 service 编写

service:提供服务的文件 service 也叫 provider,我们的业务逻辑基本上都在这里写,无论是对数据库的增删改查,还是业务逻辑的编写都在这里(当然一部分公共库还是要提取的,跟其他开发一样)

module:模块管理器,我们的应用由一个根模块和多个子模块组成,首先是根模块,然后是子模块,当然特别小的程序可能只有一个跟模块,这里比较推荐根据功能分割成多个子模块,日后方便管理

扩展: 当然后面还有什么 entity(数据库映射表)、dto(接口参数规范)、guard(鉴权)等,后面会介绍到的(也许当前文章没有)

扩展2: IOC机制,在 controller、service 中引入内容相应功能时,可以参考 module 中注入的内容,不需要 new,直接使用即可

nest指令

我们配置完成后,就可以使用 nest 指令了,如果不记得也没事,很简单,调用 -help 即可

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

nestjs-基础入门

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

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

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

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

nestjs-基础入门

路由

指的就是 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 ...
}

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

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、class-transformer
yarn add class-validator class-transformer

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

export class UserDto {
    //api属性备注,必填
    @IsNotEmpty({ message: '名称不能为空' }) //可以返回指定message,返回为数组
    readonly name: string

    //可选参数
    @IsOptional()
    @IsNotEmpty() //返回默认 message,且为字符串数组,毕竟可能存在多个为空的
    readonly age: number

    readonly mobile: string
    
    //自定义校验类型,需要自己编写校验类或者装饰器,其他文章会介绍
    @Max(2)
    @Min(0)
    readonly sex: number
    
    //收入,金额类型,两位小数,没有满足的校验需要自定义
    @Validate(IsMoney)
    income: string

    @IsBoolean()
    isMarry: boolean
}

除了上面还要在 main 函数加入下面代码,将其设为全局

app.useGlobalPipes(new ValidationPipe());

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

nestjs-基础入门

当然 getquery 也可以设置,只不过没有那么便利,或者那么校验了,只能使用默认的几种,例如:下面的 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,然后启动即可,忘记密码也可以点进去配置,比以前方便太多了

nestjs-基础入门

ps:我自动自动后,基本都是开机自启,基本不用管了(甚至时间久了都会忘了流程,毕竟太简单了😂)

连接数据库

到这里数据库服务我们开启了,但我们还没创建数据库,因此需要创建数据库(nest只会创建表和读写,不能创建数据库)

创建前我们需要下载一些工具来操作和查看数据库,可以已使用 appstore 上面的一些收费的软件,其看起来比较舒服,但需要马内,也可以使用vscode插件等,个人倾向 vscode插件,毕竟免费,丑点无所谓

打开 vscode 搜索 database client,然后下载,下载后我们打开,会出现这个界面,直接输入我们的密码和数据库类型名称即可,数据库填写 mysql,不要填写我们自定义的 database 名字,如下所示填写完成后,点,连接成功

nestjs-基础入门

到这里我们还需要创建我们的数据库 database,因此需要命令,我们新建一个,会出现下面命令,我们在 CREATE DATABASE后面加上我们自定义的 database 名字即可,这里叫 nest_demo,当然也可以创建连接多个数据库

nestjs-基础入门

到这里就完成了,后面只需要使用 typeorm 库连接我们的数据库,并配置好数据库字段映射操作即可,配置完成后,下次运行便会出现我们的表了(虾米那会介绍怎么连接和映射)

nestjs-基础入门

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 的环境变量文件,在里面填写我们用到的一些相关变量

nestjs-基础入门

创建一个 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()  //默认带的 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时一起介绍

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

//这个地址本地看得话就是 http://localhost:3000/api-docs了

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

nestjs-基础入门

配置路由、dto文档

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

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

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

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

前面给 @Body 后续前面设置了 dtodto也可以配置上传参数的标签,以便于用户设置,再加上以前的参数验证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})
    readonly sex: number
    @Min(0)
    @Max(1)
    @ApiPropertyOptional({description: '是否已婚', example: false})
    @IsNotEmpty()
    marry: number
}

body 传参如下所示,可以点击 schema 查看必填项

nestjs-基础入门

nestjs-基础入门

后面还会增加 swagger 更加详细的内容,这篇就介绍到这里了(文档很重要,单独搞一篇出来,保证能做出来一个相对比较好的 http 文档)

设置api路由前缀

有时为了使接口api更加清晰化,或预留位置等情况,我们开发时,会给我们的项目添加一个全局路由

app.setGlobalPrefix('api');

这样接口的基础地址就会变成 http://localhost:3000/api
文档还是原来那个不受影响 /api-docs

设置跨域支持

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

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

最后

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

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

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

祝大家学习愉快!