nestjs web开发整合

1,788 阅读2分钟

去年开始就关注nestjs, 最近公司采用typescrtipt, 趁着空闲时间, 学习学习nestjs, 记个笔记, 不定时更新. 但内容非完全原创. 主要参考来源以下:

参考资料: docs.nestjs.cn/6/technique… nestjs.com/ github.com/dzzzzzy/Nes… ....

没有全部列出, 要是跟各位大佬内容冲突了, 见谅哈!

初始化项目

  1. npm i -g @nestjs/cli
  2. nest new project_name

controller支持的请求方式

@nest/common组件下支持@Get, @Post, @Delete, @Put四种请求方式.

参数获取

import { Controller, Get, Param, Query, Post, Body } from "@nestjs/common";

@Controller('base')
export class BaseController {

/**
 * 获取query string  /?page=1&size=20
 * @param query
 */
@Get()
getByQuery(@Query() query: any): any {
    return query;
}

/**
 * 获取url path参数
 * @param id
 */
@Get('/:id')
getId(@Param('id') id: number): string {
    return `the user id is ${id}`;
}

/**
 * json格式 {}
 * @param body
 */
@Post()
getByBody(@Body() body: any): any {
    return body;
}
}

Providers & Dependency Injection

控制层负责处理request及response, 数据处理一般采用service 依赖注入的方式

  1. 创建service

     import { Injectable } from '@nestjs/common';
     
     @Injectable()
     export class BaseService {
    
         getOne(): any {
             return '获取到所有的用户';
         }
     }
    
  2. 在module中注册

     import { Module } from '@nestjs/common';
     import { BaseController } from './base.controller';
     import { BaseService } from './base.service';
     
     @Module({
       controllers: [BaseController],
       providers: [BaseService],// 注册服务
     })
     export class BaseModule {}
     
    
  3. 在controller层注入

    constructor(private readonly baseService: BaseService) { }
    

管道-参数校验

管道的作用:

  1. 数据转换 将输入数据转换为所需的输出

  2. 数据验证 接收客户端提交的参数,如果通过验证则继续传递,如果验证未通过则提示错误 class-validator, class-transformer组件可以实现用来验证DTO传输的参数校验 一般流程:

  3. 建立参数DTO, 使用class-validator相关的Decorators

    import { IsString, IsInt, IsEmail, IsNotEmpty } from 'class-validator';
     import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
     
     export class BaseDto {
       @IsNotEmpty({ message: '用户姓名是必不可少的', context: { errorCode: ApiErrorCode.USER_NAME_INVALID } })
       name: string;
     
       @IsInt({ message: '用户年龄必须是整数', context: { errorCode: ApiErrorCode.USER_ID_INVALID} })
       age: number;
     
       @IsEmail({}, { message: '必须满足邮箱格式', context: { errorCode: ApiErrorCode.USER_EMAIL_INCALID}})
       email: string;
     }
    
  4. 建立对应验证类, 实现PipeTransform的transfrom方法

     import { PipeTransform, ArgumentMetadata, Injectable, HttpException, HttpStatus } from '@nestjs/common';
     import { plainToClass } from 'class-transformer';
     import { validate } from 'class-validator';
     import { ApiException } from 'src/common/exception/api.exception';
     
     @Injectable()
     export class BaseFormDTOValidationPipe implements PipeTransform<any> {
         async transform(value, { metatype }: ArgumentMetadata) {
             if (!metatype || !this.toValidate(metatype)) {
               return value;
             }
             const object = plainToClass(metatype, value);
             const errors = await validate(object);
             if (errors.length > 0) {
                // 获取到第一个没有通过验证的错误对象
             const error = errors.shift();
             const constraints = error.constraints;
             const contexts = error.contexts;
             // 将未通过验证的字段的错误信息和状态码,以ApiException的形式抛给我们的全局异常过滤器
             for (const key of Object.keys(constraints)) {
               throw new ApiException(constraints[key], contexts[key].errorCode, HttpStatus.BAD_REQUEST);
               }
             }
             return value;
         }
         private toValidate(metatype): boolean {
             const types = [String, Boolean, Number, Array, Object];
             return !types.find((type) => metatype === type);
           }
     }
    
  5. 建立shared服务模块, 并注册

     import { Module } from '@nestjs/common';
     import { BaseFormDTOValidationPipe } from './BaseDTOValidationPipe';
     
     @Module({
         providers: [
             BaseFormDTOValidationPipe,
         ],
     })
     export class SharedModule {}
    
  6. 在app.module.ts模块注册该shared服务模块

     @Module({
       imports: [BaseModule, SharedModule],
       controllers: [AppController],
       providers: [AppService],
     })
     export class AppModule {}
     
    
  7. 使用:

在对应的controllers加上@UsePipes注解, 参数为对应创建的验证类

 @Post()
    @UsePipes(BaseFormDTOValidationPipe)
    getByBody(@Body() baseDto: BaseDto): any {
        // tslint:disable-next-line: no-console
        console.log(baseDto);
        return baseDto;
    }
}

中间件middleware

client -> middleware -> routing handler

  • 可以执行任何代码
  • 对请求和响应进行拦截
  • 结束请求-响应周期
  • 调用堆栈下一个中间件函数
  • 下一个中间件必须调用next()传递控制
  1. 定义中间件类,实现NestMiddleware的方法

     import { NestMiddleware, Injectable } from '@nestjs/common';
     import { Request, Response } from 'express';
     
     @Injectable()
     export class LoggerMiddleware implements NestMiddleware {
         use(req: Request, res: Response, next: () => void) {
             // tslint:disable-next-line: no-console
             console.log(req.url);
             // tslint:disable-next-line: no-console
             console.log('记录');
             next();
         }
     }
    
  2. 依赖注入

注意: 中间件不能在@Module()装饰器列出, 使用模块configure()方法来设置, 包含中间件模块必须实现NestModule接口

    export class AppModule implements NestModule {
      configure(consumer: MiddlewareConsumer) {
        consumer
        .apply(LoggerMiddleware) // 指定要套用哪个Middleware,可以套用多个middleware,以逗點分隔
        .exclude()
        .forRoutes(BaseController); // 指定哪些routes要套用,可以传入Controller或是Controller数组
      }
    }
    

3. 如果要配置全局中间件 const app = await NestFactory.create(ApplicationModule); app.use(LoggerMiddleware); await app.listen(3000);

统一异常处理横切面过滤器

  1. 自定义异常类继承ExceptionFilter

     import { ExceptionFilter, ArgumentsHost, Catch, HttpException } from '@nestjs/common';
     import { ApiException } from './api.exception';
    
     @Catch(HttpException)
     export class HttpExceptionFilter implements ExceptionFilter {
         catch(exception, host: ArgumentsHost) {
             const ctx = host.switchToHttp();
             const response = ctx.getResponse();
             const request = ctx.getRequest();
             const status = exception.getStatus();
             if (exception instanceof ApiException) {
                 response.status(status).json({
                     errorCode: exception.getErrorCode(),
                     errorMessage: exception.getErrorMessage(),
                     date: new Date().toLocaleDateString(),
                     path: request.url,
                 });
             } else {
                 response.status(status).json({
                     statusCode: status,
                     date: new Date().toLocaleDateString(),
                     path: request.url,
                     error: exception,
                 });
             }
         }
     }
    
  2. 自定义接口异常继承HttpException

     import { HttpException, HttpStatus } from '@nestjs/common';
     import { ApiErrorCode } from '../enums/api-error-code.enum';
     
     export class ApiException extends HttpException {
         private errorMessage: string;
         private errorCode: ApiErrorCode;
     
         constructor(errorMessage: string, errorCode: ApiErrorCode, statusCode: HttpStatus) {
             super(errorMessage, statusCode);
             this.errorMessage = errorMessage;
             this.errorCode = errorCode;
         }
     
         getErrorCode(): ApiErrorCode {
             return this.errorCode;
         }
     
         getErrorMessage(): string {
             return this.errorMessage;
         }
     }
    
  3. 注册

     import { NestFactory } from '@nestjs/core';
     import { AppModule } from './app.module';
     import { HttpExceptionFilter } from './common/exception/http.exception.filter';
     
     async function bootstrap() {
       const app = await NestFactory.create(AppModule);
       app.useGlobalFilters(new HttpExceptionFilter());
       await app.listen(3000);
     }
     bootstrap();
    
  4. 在控制层可以通过throw new ApiException()来抛出异常.

  5. 自定义错误码

     export enum ApiErrorCode {
         TIMEOUT = -1, // 系统繁忙
         SUCCESS = 0, // 成功
     
         USER_ID_INVALID = 10001, // 用户id无效
     }
    
  6. 最佳方式是每一个控制器的异常通过继承ApiException来抛出异常.

统一的返回格式

  1. 定义一个返回格式的接口

         /**
          * 请求正确返回的json格式的interface
          */
         export interface ResponseData {
             code: number;
             msg: string;
             data: any;
             time: string;
         }
    
  2. 定义一个统一返回的类

     import { ResponseData } from '../interface/json.data';
     
     export class JsonData {
         static success(data: any): ResponseData {
             const result: ResponseData = {
                 code: 10,
                 msg: 'success',
                 data,
                 time: new Date().toLocaleString(),
             };
             return result;
         }
     
         static fail(code: number, msg: string) {
             const result: ResponseData = {
                 code: 0,
                 msg,
                 data: undefined,
                 time: new Date().toLocaleString(),
             };
             return result;
         }
     }
    

文件上传

  1. file.controller.ts

     import { Post, Controller, UploadedFile, UseInterceptors, Body } from '@nestjs/common';
     import { FileInterceptor } from '@nestjs/platform-express';
     import { UploadFileService } from './file.service';
     
     @Controller('upload')
     @UseInterceptors(FileInterceptor('file', {
         limits: {
             fieldSize: 1024 * 1024,
             fileSize: 1024 * 1024,
         },
     }))
     export class FileUploadController {
         constructor(private readonly uploadFileService: UploadFileService) {}
         @Post('/file')
         uploadFile(@UploadedFile() file, @Body() body): any {
             // tslint:disable-next-line: no-console
             console.log(file);
             // tslint:disable-next-line: no-console
             console.log(body.name);
             return this.uploadFileService.uploadFile(file);
         }
     }
    
  2. file.service.ts

     import { Injectable, HttpStatus } from '@nestjs/common';
     import { SingleFile } from './file.interface';
     import { CryptoUtil } from 'src/common/util/hmac.crytpo';
     import { join, extname } from 'path';
     import { createWriteStream } from 'fs';
     import { ApiException } from 'src/common/exception/api.exception';
     import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
    
     /**
      * 文件传输controller
      */
     @Injectable()
     export class UploadFileService {
     
         /**
          * 单个文件传输
          * @param file
          */
         public uploadFile(file: SingleFile): string {
             try {
                 const { originalname, buffer} = file;
                 const extName = extname(originalname);
                 const md5FileName = CryptoUtil.MD5Encrypt(buffer);
                 const fullName = md5FileName + extName;
                 const filePath = join(__dirname, '../', uploadBasePath, fullName);
                 const writeStream = createWriteStream(filePath);
                 writeStream.write(buffer);
                 return uploadBasePath + '/' + fullName;
             } catch (err) {
                 throw new ApiException('文件上传错误', ApiErrorCode.UPLOAD_FILE_ERROR, HttpStatus.FORBIDDEN);
             }
         }
     }
     
     const uploadBasePath: string = 'public/uploads';
    

整合mysql ----- typeorm

初始化配置

  1. $ npm install --save @nestjs/typeorm typeorm mysql

  2. 项目根目录配置数据库连接信息

     {
         "type": "mysql",
         "host": "localhost",
         "port": 3306,
         "username": "root",
         "password": "root",
         "database": "test",
         "entities": ["src/**/*.entity{.ts}"],
         "synchronize": true,
         "logging": true
       }
    

user模块

  1. user.module.ts

    import { Module } from '@nestjs/common';
    import { UserController } from './user.controller';
    import { UserService } from './user.service';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { UserEntity } from './user.entity';
    
    @Module({
        imports: [TypeOrmModule.forFeature([UserEntity])],
        controllers: [UserController],
        providers: [UserService],
    })
    export class UserModule {}
    
  2. user.dto.ts

    import { IsNotEmpty, MaxLength, IsMobilePhone, IsEmail} from 'class-validator';
    import { ApiErrorCode } from 'src/common/enums/api-error-code.enum';
    
    export class UserDto {
        @IsNotEmpty({message: '用户名不能为空', context: { errorCode: ApiErrorCode.USER_NAME_INVALID}})
        @MaxLength(20, {message: '最大长度奴能超过20', context: { errorCode: ApiErrorCode.USER_NAME_INVALID}})
        public name: string;
        @IsMobilePhone('zh-CN', {message: '手机号不合法', context: { errorCode: ApiErrorCode.USER_PHONE_INVALID}})
        public phone: string;
        @IsEmail({}, { message: '邮箱不合法', context: {errorCode: ApiErrorCode.USER_EMAIL_INVALID}})
        public email: string;
        @IsNotEmpty({message: '密码不能为空', context: {errorCode: ApiErrorCode.USER_PASSWORD_INVALID}})
        public password: string;
    }
    
  3. user.entity

    import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
    
    @Entity('user')
    export class UserEntity {
        @PrimaryGeneratedColumn({name: 'id', comment: '主键自增id'})
        id: number;
    
        @Column({length: 20, name: 'name', comment: '用户名'})
        name: string;
    
        @Column({name: 'email', length: 32, comment: '邮箱'})
        email: string;
    
        @Column({name: 'password', length: 64, comment: '密码'})
        password: string;
    }
    
  4. user.service.ts

    import { Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { UserEntity } from './user.entity';
    import { Repository } from 'typeorm';
    import { UserDto } from './use.dto';
    
    @Injectable()
    export class UserService {
        constructor(
            @InjectRepository(UserEntity)
            private readonly userRepository: Repository<UserEntity>,
        ) {}
    
        async create(userDto: UserDto): Promise<number> {
            const user = new UserEntity();
            user.name = userDto.name;
            user.password = userDto.password;
            user.email = userDto.email;
            const result = await this.userRepository.save(this.userRepository.create(user));
            return result.id;
        }
    }
    
  5. user.controller.ts

    import { Controller, Body, Post, UsePipes } from '@nestjs/common';
    import { UserDto } from './use.dto';
    import { BaseFormDTOValidationPipe } from 'src/shared/BaseDTOValidationPipe';
    import { UserService } from './user.service';
    import { UserEntity } from './user.entity';
    
    @Controller('user')
    export class UserController {
        constructor(private readonly userService: UserService) {}
    
        @Post()
        @UsePipes(BaseFormDTOValidationPipe)
        async create(@Body() userDto: UserDto): Promise<number> {
            // tslint:disable-next-line: no-console
            return await this.userService.create(userDto);
        }
    }
    

CRUD操作

nestjs 调式

新建 .vscode 文件夹,新增launch.json文件内容如下

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Attach NestJS WS",
      "port": 9229,
      "restart": true,
      "stopOnEntry": false,
      "protocol": "inspector",
      "skipFiles": [
         "<node_internals>/**/*.js",
         "${workspaceRoot}/node_modules/**/*.js",
       ]
    }
  ]
}

npm run start:debug

启动调试