NestJS 7.x 折腾记: (5) 管道,一个好玩的东西!比如入参校验!

2,033 阅读4分钟

前言


管道这个词,若是有玩过Linux的小伙伴肯定知晓,比如(看图):
image.png
意思就git的提交记录以单行显示的前三条,而且提交概要中有build这个词的~

在nest里面的管道,主要是用来做入参数据类型的拦截转换;
跟ng提供的概念差不多,大同小异~~

image.png

效果图

2020-11-13 16.38.52.gif实战

其实官方教程写的挺好了,
局部管道和全局管道都写到了,
这里我们以更具通用性的入参全局管道做例子,
尽量写的更贴近业务和更为粗俗的解释吧~

安装

# class-validator: 提供非常丰富的类型装饰器
# class-transformer: 可以把常规数据快速转成类~
# https://www.npmjs.com/package/class-validator
# https://www.npmjs.com/package/class-transformer
yarn add class-validator class-transformer

封装入参拦截管道

Usage: nest <command> [options]

Options:
  -v, --version                                   Output the current version.
  -h, --help                                      Output usage information.

Commands:
  new|n [options] [name]                          Generate Nest application.
  build [options] [app]                           Build Nest application.
  start [options] [app]                           Run Nest application.
  info|i                                          Display Nest project details.
  update|u [options]                              Update Nest dependencies.
  add [options] <library>                         Adds support for an external library to your project.
  generate|g [options] <schematic> [name] [path]  Generate a Nest element.
    Available schematics:
      ┌───────────────┬─────────────┬───────────────────────────────���──────────────┐
      │ name          │ alias       │ description                                  │
      │ application   │ application │ Generate a new application workspace         │
      │ class         │ cl          │ Generate a new class                         │
      │ configuration │ config      │ Generate a CLI configuration file            │
      │ controller    │ co          │ Generate a controller declaration            │
      │ decorator     │ d           │ Generate a custom decorator                  │
      │ filter        │ f           │ Generate a filter declaration                │
      │ gateway       │ ga          │ Generate a gateway declaration               │
      │ guard         │ gu          │ Generate a guard declaration                 │
      │ interceptor   │ in          │ Generate an interceptor declaration          │
      │ interface     │ interface   │ Generate an interface                        │
      │ middleware    │ mi          │ Generate a middleware declaration            │
      │ module        │ mo          │ Generate a module declaration                │
      │ pipe          │ pi          │ Generate a pipe declaration                  │
      │ provider      │ pr          │ Generate a provider declaration              │
      │ resolver      │ r           │ Generate a GraphQL resolver declaration      │
      │ service       │ s           │ Generate a service declaration               │
      │ library       │ lib         │ Generate a new library within a monorepo     │
      │ sub-app       │ app         │ Generate a new application within a monorepo │
      │ resource      │ res         │ Generate a new CRUD resource    
# 会在src/common/pipes/生成  validation.pipe.ts
# 根路径在nest-cli.json配置!默认就是src为开发根目录

nest n pi common/pipes/validation

管道实现

// src/pipe/validation.pipe.ts
import {
  ArgumentMetadata,
  BadRequestException,
  Injectable,
  PipeTransform,
} from '@nestjs/common';
// plainToClass 会把一个普通的js对象转换成指定类的实例
import { plainToClass } from 'class-transformer';
// 可以识别校验装饰器数据
import { validate } from 'class-validator';

Injectable();
export class ValidationPipe implements PipeTransform {
  // value 就是传入的实际数据
  // metatype 就是元数据,其实就是装饰器添加那些
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      // 如果没有传入验证规则,则不验证,直接返回数据
      return value;
    }
    // 将对象转换为 Class 来验证
    const object = plainToClass(metatype, value);
    // 同步阻塞,返回校验结果
    const errors = await validate(object);
    if (errors.length > 0) {
      // 只需要取第一个错误信息并返回即可
      const msg = Object.values(errors[0].constraints)[0]; 
      // 抛出这个异常,逻辑就会交付nest的错误拦截去了
      // 要拦截这个错误做处理,可以从filters入手,以后会说到
      throw new BadRequestException(`字段校验不通过: ${msg}`);
    }
    return value;
  }
  // 这个函数的意义就是验证元数据传入的类型是否是定义内的常规类型数据
  private toValidate(metatype: any): boolean {
    const types: any[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

配置

主入口(main.ts)

import { AppModule } from './app.module';
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from './common/pipes/validataion.pipe';
async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    cors: false,
    logger: false,
  });

  // 设置全局管道
  app.useGlobalPipes(new ValidationPipe());

  await app.listen(configService.get('SERVE_LISTENER_PORT'));
}
bootstrap()

DTO注解

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
  IsInt,
  IsNumberString,
  IsOptional,
  IsString,
  Max,
  Min,
} from 'class-validator';

export enum UserRole {
  Boss = '后门',
  Admin = '管理员',
  User = '常规用户',
}

export class CreateAppDto {
  @ApiProperty({ enum: ['Boss', 'Admin', 'User'] })
  role: UserRole;

  @IsOptional()
  @IsString({
    message: '用户名必须为字符串',
  })
  @ApiPropertyOptional({
    description: '姓名',
  })
  readonly name?: string;

  @IsInt()
  @Min(10, { message: '年龄下限为10' })
  @Max(130, { message: '年龄上限130' })
  @ApiProperty({
    description: '年龄',
    minimum: 0,
    maximum: 130,
    required: false,
  })
  readonly age: number;

  @IsString({
    message: '爱好必须为字符串',
  })
  @ApiPropertyOptional({
    description: '爱好',
  })
  readonly hobit: string;
}

export class FindOneParams {
  @IsNumberString()
  id: number;
}

Controller

import { Controller, Get, Post, HttpCode, Body, Query } from '@nestjs/common';
import {
  ApiCreatedResponse,
  ApiHeader,
  ApiInternalServerErrorResponse,
  ApiOkResponse,
  ApiOperation,
  ApiParam,
  ApiQuery,
  ApiResponse,
} from '@nestjs/swagger';
import { CreateAppDto, FindOneParams, UserRole } from './app.dto';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @ApiHeader({
    name: 'Authorization',
    description: 'Auth token',
  })
  @ApiCreatedResponse({
    description: '链接成功创建,其实就是201状态的描述',
  })
  @Post('/post')
  @HttpCode(200)
  @ApiParam({ name: 'name', description: '名字', type: CreateAppDto })
  postParams(@Body() param: CreateAppDto): string {
    return '测试参数' + JSON.stringify(param);
  }

  @Get('/user')
  @ApiOperation({
    tags: ['获取用户信息'],
    description: '获取用户信息',
    deprecated: true,
  })
  @ApiQuery({ name: 'id', description: '用户id' })
  @ApiResponse({ description: '成功请求回来,其实就是200的描述', status: 200 })
  @ApiInternalServerErrorResponse({ description: '服务端异常' })
  updateApp(@Query() query: FindOneParams) {
    return JSON.stringify(query);
  }

  @Get('/netease-news/:id')
  @ApiOkResponse({ description: '成功请求回来' })
  @ApiQuery({ name: 'id', description: '用户id', required: false })
  async async(@Body() body) {
    const res = await this.appService.getNetEaseNew(
      'https://anapioficeandfire.com/api/characters/583',
      { data: body },
    );
    return res.data;
  }

  @ApiQuery({ name: 'role', enum: UserRole })
  @ApiOperation({
    tags: ['返回角色信息'],
    description: '返回角色信息',
  })
  @Get('/role')
  async filterByRole(@Query('role') role: UserRole = UserRole.User) {
    return role;
  }
}

总结

有不对之处请留言,会及时修正!谢谢阅读~