NestJS学习(一)SwaggerUI深度挖掘

1,677 阅读5分钟

感谢大佬的Nest.js系列

GitHub项目地址,☆☆☆☆☆

为什么学习NestJS

你以为我会跟你说些大道理吗?你错了,我的故事是这样的。。。

那晚,我还在畅快的玩着手机,媳妇走过来了,说:”老公,你不是做IT的吗,我想做个项目,感觉很有前景的样子,你也可以自己练练手“ 然后就这样了

image.png

我的工作范围从一个专业Bug前端程序猿 到 修手机 到 修电脑 然后又到了 写后台做整个项目了。为了不影响家庭和睦,我也是抱着练练手的态度,跟媳妇在床上畅聊了一夜。。。

进入正题

这是我们家Java写的,反正我看着挺爽

image.png

我的最终目标,虽然没有java搞的漂亮,但是我尽力了

image.png

起步

关于SwaggerUI,别的装饰器在这里就不写了,大家可以参考文章开始推荐的链接。直接3挡起步,Zoom~!Zoom~!Zoom~!

@ApiResponse({ status: 200, type: UserItemDto, isArray: true })

刚开始 我的文档返回示例是这个鸟样子,感觉少了点什么。哦~~ code 和 msg 呢?

image.png

深挖1

没关系,我们有泛型,机智如我

content.dto.ts

class ResponseDto {
  @ApiProperty()
  code: number
  @ApiProperty()
  msg: string
}

export class ResponseObjDto<TData> extends ResponseDto {
  @ApiProperty()
  data: TData
}

export class ResponseListDto<TData> extends ResponseDto {
  @ApiProperty()
  data: TData[]
}

她好像让我new一下,满足她!

image.png

然后,她不愿意,还说我不是她稀饭的类型

image.png

深挖2

我艹,不支持泛型!突然感觉知识很匮乏,幸好官方文档给出了方案,需要使用 Schema

@ApiOkResponse({
  schema: {
    allOf: [
      { $ref: getSchemaPath(ResponseListDto) },
      {
        properties: {
          data: {
            type: 'array',
            items: { $ref: getSchemaPath(UserItemDto) },
          },
        },
      },
    ],
  },
})
async findAll() {
  return await this.userService.findAll()
}
  • getSchemaPath() 函数从一个给定模型的OpenAPI指定文件返回OpenAPI原型路径
  • allOf是一个OAS3的概念,包括各种各样相关用例的继承。

看,她来了~!😄

image.png

深挖3

你以为这样就好了吗?官网都说了,你不得封装一下子,来复用啊?!于是乎我就有了下面这三个自定义装饰器 公共的DTO我都定义在了这里 content.dto.ts

api-list-response.decorator.ts

import { applyDecorators, Type } from '@nestjs/common'
import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger'
import { ResponseListDto } from '../logical/content/dto/content.dto'

export const ApiListResponse = <TModel extends Type<any>>(model: TModel) => {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        title: '响应示例',
        allOf: [
          { $ref: getSchemaPath(ResponseListDto) },
          {
            properties: {
              code: { type: 'number', default: '0' },
              msg: { type: 'string', default: 'success' },
              data: {
                type: 'array',
                items: { $ref: getSchemaPath(model) },
              },
            },
          },
        ],
      },
    }),
  )
}

api-obj-response.decorator.ts

import { applyDecorators, Type } from '@nestjs/common'
import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger'
import { ResponseObjDto } from '../logical/content/dto/content.dto'

export const ApiObjResponse = <TModel extends Type<any>>(model: TModel) => {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        title: '响应示例',
        allOf: [
          { $ref: getSchemaPath(ResponseObjDto) },
          {
            properties: {
              code: { type: 'number', default: '0' },
              msg: { type: 'string', default: 'success' },
              data: {
                $ref: getSchemaPath(model),
              },
            },
          },
        ],
      },
    }),
  )
}

套娃现场👇👇👇👇👇

api-paginated-response.decorator.ts

import { applyDecorators, Type } from '@nestjs/common'
import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger'
import {
  PaginatedDto,
  ResponsePaginatedDto,
} from '../logical/content/dto/content.dto'

export const ApiPaginatedResponse = <TModel extends Type<any>>(
  model: TModel,
) => {
  return applyDecorators(
    ApiOkResponse({
      schema: {
        title: '响应示例',
        allOf: [
          { $ref: getSchemaPath(ResponsePaginatedDto) },
          {
            properties: {
              code: { type: 'number', default: '0' },
              msg: { type: 'string', default: 'success' },
              data: {
                allOf: [
                  { $ref: getSchemaPath(PaginatedDto) },
                  {
                    properties: {
                      results: {
                        type: 'array',
                        items: { $ref: getSchemaPath(model) },
                      },
                    },
                  },
                ],
                type: 'object',
                items: { $ref: getSchemaPath(PaginatedDto) },
              },
            },
          },
        ],
      },
    }),
  )
}

上了之后,她更美了🤓

@ApiListResponse(UserItemDto)
async findAll() {
  return await this.userService.findAll()
}
@ApiPaginatedResponse(UserItemDto)
async findAll() {
  return await this.userService.findAll()
}
@ApiObjResponse(UserItemDto)
async findOne(@Query('username') username) {
  return await this.userService.findOne(username)
}

贴个分页的示例吧

image.png

深挖4 - CLI Plugin

到了这里,我决定扛着铁锹再挖一下,有意想不到的收获(以下内容来自 NestJS中文文档)因为有你们,我才可以安心的当个屌丝

Swagger插件可以自动:

  • 使用@ApiProperty注释所有除了用@ApiHideProperty装饰的DTO属性。
  • 根据问号符号确定required属性(例如 name?: string 将设置required: false)
  • 根据类型配置typeenum(也支持数组)
  • 基于给定的默认值配置默认参数
  • 基于class-validator装饰器配置一些验证策略(如果classValidatorShim配置为true)
  • 为每个终端添加一个响应装饰器,包括合适的状态和类型(响应模式)
  • 根据注释生成属性和终端的描述(如果introspectComments配置为true)
  • 基于注释生成属性的示例数据(如果introspectComments配置为true)

注意,你的文件名必须有如下后缀: ['.dto.ts', '.entity.ts'] (例如create-user.dto.ts) 才能被插件分析

  1. 首先你要启用Swagger插件

nest-cli.json

{
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "plugins": [
      {
        "name": "@nestjs/swagger",
        "options": {
          "classValidatorShim": false,
          "introspectComments": true,
          "controllerKeyOfComment": "summary"
        }
      }
    ]
  }
}

Opiton选项:

OptionDefaultDescription
dtoFileNameSuffix['.dto.ts', '.entity.ts']DTO (数据传输对象)文件后缀
controllerFileNameSuffix.controller.ts控制文件后缀
classValidatorShimtrue如果设置为true,则模块将重用class-validator验证装饰器(例如,@max(10)将max:10添加到schema定义)
dtoKeyOfComment'description'将注释文本设置为ApiProperty的属性键。 
controllerKeyOfComment'description'将注释文本设置为ApiOperation的属性键。
introspectCommentsfalse如果配置为true,插件将根据描述注释生成说明和示例
  1. 然后我就开始对DTO下手了。。。 脱了她们一层”衣服“ 👗👠👒 👉👉 👙
export class UserItemDto extends ContentDto {
  /**
   * 用户名
   * @example Chaos
   */
  @IsNotEmpty({ message: '用户名不能为空' })
  @IsString({ message: '用户名必须是 String 类型' })
  username?: string

  /**
   * 邮箱
   */
  email?: string
  
  ...此处省略1万行

  /**
   * 用户状态: 1-可用 0-不可用
   */
  status: number

  /**
   * 手机号
   * @example 18383838383
   */
  @IsNotEmpty({ message: '手机号不能为空' })
  phone: string

  /**
   * 真实姓名
   * @example: '郭百万'
   */
  @IsNotEmpty({ message: '真实姓名不能为空' })
  @IsString({ message: '真实姓名必须是 String 类型' })
  trueName: string
}
  1. 然后又一阵翻云覆雨,就可以这么漂亮了

image.png

同样的,控制器中的方法也可以去掉一个ApiOperation装饰器了

/**
 * 根据用户名查询用户信息
 * @param username
 */
@Get()
// @ApiOperation({ description: '根据用户名查询用户信息' })
@ApiQuery({ name: 'username', description: '用户名' })
@ApiObjResponse(UserItemDto)
async findOne(@Query('username') username) {
  return await this.userService.findOne(username)
}

深挖4

报应来了,Swagger文档报错了,虽然不影响使用,但是我坐不住了

Resolver error at paths./lease-mgr/user/list.get.responses.200.content.application/json.schema.allOf.0.$ref Could not resolve reference: #/components/schemas/ResponseListDto

image.png 阅读文档过程中有强调过,但是当时并没有明白这样做的用意,然后就有了血的教训。就感觉NestJS文档鄙视了我一下,不听老人言,死在我面前。

因为ResponseListDto没有被任何控制器直接引用,SwaggerModule还不能生成一个相应的模型定义。我们需要一个额外的模型,可以在控制器水平使用@ApiExtraModels()装饰器。

@ApiTags('user')
@ApiBearerAuth()
@Controller('user')
@ApiExtraModels(
  PaginatedDto,
  UserItemDto,
  ResponseObjDto,
  ResponseListDto,
  ResponsePaginatedDto,
)
export class UserController {
    ...
}

深挖5

搁浅了。。。

其实想把文档搞成我们家Java的那样,奈何没有找到解决方案,希望可以有高人来指点!

总结

对于大佬们来说,可能研究的还不算深。我说深 她就深!要你管!?还请大佬们多多指点,多多担待,自己学习的路还很长。