从零开始开发聊天APP (后端篇一)

120 阅读3分钟

创建项目

注意:node 18+ 以上

// 安装cli
npm i -g @nestjs/cli
nest new server 
npm run start:dev

访问 http://localhost:3000 显示Hello World!启动成功

连接数据库

yarn add @nestjs/mongoose mongoose @nestjs/config @nestjs/mapped-types

根目录新建 config/env.ts

import * as fs from 'fs';
import * as path from 'path';
const isProd = process.env.NODE_ENV === 'production';

function parseEnv() {
  const localEnv = path.resolve('.env');
  const prodEnv = path.resolve('.env.prod');

  if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
    throw new Error('缺少环境配置文件');
  }

  const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
  return { path: filePath };
}
export default parseEnv();

根路径新建.env

// 数据库地址
DB_HOST=localhost
// 数据库端口
DB_PORT=27017
// 数据库登录名
// DB_USER=root
// 数据库登录密码
// DB_PASSWORD=123456
// 数据库名字
DB_DATABASE=my-chat-app

// secret
SECRET=my-chat-app

app.module.ts

import { Module } from '@nestjs/common';  
import { AppController } from './app.controller';  
import { AppService } from './app.service';  

+ import { ConfigService, ConfigModule } from '@nestjs/config';  
+ import { MongooseModule } from '@nestjs/mongoose';  
+ import envConfig from '../config/env';
  
@Module({  
imports: [
   + ConfigModule.forRoot({  
   +     isGlobal: true,  
   +     envFilePath: [envConfig.path],  
   + }),  
   + MongooseModule.forRootAsync({  
   +     imports: [ConfigModule],  
   +     inject: [ConfigService],  
   +     useFactory: async (configService: ConfigService) => ({  
   +         uri: `mongodb://${configService.get('DB_HOST')}:${configService.get(  
   +         'DB_PORT',  
   +         )}/${configService.get('DB_DATABASE', '')}`,  
   +         })  
   +     }),  
   + }),
],  
controllers: [AppController],  
providers: [AppService],  
})  
export class AppModule {}

新增user模块

   // 命令行执行会自动创建用户模块
   nest g res user
   选择REST API 回车
   
   注意:如果有报错生成不出文件则额外执行
   yarn add @nestjs/schematics -D

添加参数验证方法

yarn add class-transformer class-validator reflect-metadata uuid
yarn add @types/uuid -D

user/dto/create-user.dto.ts 添加校验规则

+ import { IsNotEmpty, IsString } from 'class-validator';
export class CreateUserDto {
    + @IsNotEmpty({ message: '用户名必填' })  
    + @IsString({ message: '用户名必填' })  
    + readonly username: string;  

    + @IsNotEmpty({ message: '昵称必填' })  
    + @IsString({ message: '昵称必填' })  
    + readonly name: string;  

    + @IsNotEmpty({ message: '密码必填' })  
    + @IsString({ message: '密码必填' })  
    + readonly password: string;
}

main.ts 开启验证

...
+ import { ValidationPipe } from '@nestjs/common';
...
const app = await NestFactory.create(AppModule);  
+ app.useGlobalPipes(new ValidationPipe());  
await app.listen(3000);
...

user/entities/user.entity.ts 添加用户实体类

import mongoose from 'mongoose';  
  
const userSchema = new mongoose.Schema(  
{  
name: {  
    type: String,  
    required: false,  
},  
username: {  
    type: String,  
    required: true,  
    unique: true,  
},  
image: {  
    type: String,  
},  
password: {  
    type: String,  
    required: [true, '请输入密码'],  
    minlength: [6, '密码最小长度6个字符'],  
},  
// followingIds:{  
    // type: [  
    // {  
    // type: mongoose.Schema.Types.ObjectId,  
    // ref: 'User',  
    // }  
    // ],  
    // default: []  
// },  
avatar: {  
    type: String,  
},  
remark: {  
    type: String,  
    default: '',  
},  
conversationIds: [  
    {  
        type: mongoose.Schema.Types.ObjectId,  
        ref: 'Conversation',  
        default: [],  
    },  
],  
},  
{ timestamps: true },  
);  
  
export default userSchema;  
  
export type UserType = {  
    _id: string;  
    name: string;  
    username: string;  
    password: string;  
    // followingIds:UserType[]  
    avatar: string;  
    image: string;  
    status: string;  
    remark: string;  
    conversationIds: string[];  
    createdAt: string;  
    updatedAt: string;  
};

user/user.modules.ts

    import { Module } from '@nestjs/common';  
    import { UserService } from './user.service';  
    import { UserController } from './user.controller';  
    + import { MongooseModule } from '@nestjs/mongoose';;  
    + import userSchema from './entities/user.entity';
    @Module({
    // 导入User实体
    + imports: [MongooseModule.forFeature([{ name: 'Users', schema: userSchema }])],  
    controllers: [UserController],  
    providers: [UserService],  
    // 导出当前的Service 便于其他模块调用
    + exports: [UserService],  
    })  
    export class UserModule {}

user/user.service.ts 添加注册方法

...
+ import { InjectModel } from '@nestjs/mongoose';  
+ import { Model } from 'mongoose';  
+ import { UserType } from './entities/user.entity';
...
@Injectable()  
export class UserService {
... 
  +  constructor(  
  +      @InjectModel('Users') private readonly userModel: Model<UserType>,  
  +  ) {}  
  +  async create(createUserDto: CreateUserDto) {  
  +      const { username } = createUserDto;  
  +      const existUser = await this.userModel.findOne({  
  +          username,  
  +      });  
  +      if (existUser) {  
  +      throw new HttpException('用户名已存在', HttpStatus.BAD_REQUEST);  
  +      }  
  +      const newUser = await this.userModel.create(createUserDto);  
  +      return newUser._id; 
+ }
...

测试接口

上文由nest g res user 指令生成模块默认生成多个接口

访问

user POST 创建

user GET 查询 ...

不出意外将会添加成功如下: database_01.png

request_01.png

接口参数校验可以看到也已经生效

validate_01.png

添加全局相应拦截

可以看到请求成功后返回的数据格式并不是我们想要的,所以添加一个全局的相应拦截格式化一下

跟路径新建文件夹 common/interceptor

// 生成拦截器
nest g interceptor common/interceptor/response

app.module.ts 添加生成的拦截器
...
providers: [  
    AppService,  
    + {  
    + provide: APP_INTERCEPTOR,  
    + useClass: ResponseInterceptor,  
    + },  
],

// 生成的拦截器文件中添加
common/interceptor/response/response.interceptor.ts
...
+ import { map, Observable } from 'rxjs';

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {  
    ...
    - return next.handle()
    + return next.handle().pipe(  
    +    map((data) => ({  
    +    data,  
    +    code: 200,  
    +    status: 200,  
    +    msg: '操作成功',  
    + })),
    ...
);  
}

可以看到请求成功后返回的结果已经处理成功

image.png

添加异常相应拦截

请求异常时也需要添加格式化

// 生成拦截器
nest g filter common/interceptor/http-exception

app.module.ts 添加生成的异常拦截器
...
providers: [  
    AppService,  
    + {  
    + provide: APP_FILTER,  
    + useClass: HttpExceptionFilter,  
    + },  
    {  
        provide: APP_INTERCEPTOR,  
        useClass: ResponseInterceptor,  
    },
    ...
],

// 生成的拦截器文件中添加
// 这里我使用了lodash处理 exception 数据可以使用其他的库或其他方法处理
common/interceptor/http-exception/http-exception.filter.ts

+ import {ArgumentsHost,Catch,ExceptionFilter,HttpException} from '@nestjs/common';
+ import { Request, Response } from 'express';  
+ import { get } from 'lodash';

...
catch(exception: HttpException, host: ArgumentsHost) {  
   + const ctx = host.switchToHttp();  
   + const request = ctx.getRequest<Request>();  
   + const response = ctx.getResponse<Response>();  
   + const status = exception.getStatus();  

   + const data = exception.getResponse();  

   + if (status === 400 && typeof data === 'object') {  
   +    const msgs = get(data, 'message', exception.message);  
   +     const msg = Array.isArray(msgs) ? msgs.join(',') : msgs;  
   +     response.status(200).json({  
   +     code: status,  
   +     msg,  
   + });  
   + } else {  
   + response.status(200).json({  
   +     code: status,  
   +     msg: data,  
   + });  
}
....

request_03.png

request_04.png

未完待续...

下一章将继续完善基础模块 ^-^