NestJS 7.x 折腾记: (4) Swagger接入及相关用法

3,706 阅读5分钟

前言

swagger这东东,萝卜青菜各有所爱吧.

反正我呆的公司用这个,我用的也还行!
有兴趣的可以瞅瞅~

说说优点吧,
可以精确的展示每个字段意义,只要注解写的到位!
schema也能正常读取!还能直接测试接口!

效果图

以下就是配置好及写一些demo接口所展示的效果;
包括语法高亮,api分组,响应注解,api废弃,接口概述等~
image.png

实战

安装

# 前者是swagger的nest module,官方团队维护的
# 后者是适配express的swagger ui库
# 库用新不用旧,语法会有所差异!
yarn add @nestjs/swagger swagger-ui-express

配置

抽离的环境变量(dev.local.env)

# ------- Node服务相关 ---------------------
# Node服务启动监听的端口
SERVE_LISTENER_PORT=3000



# ------- Swagger相关 ---------------------
# Swagge Api文档访问路径
SWAGGER_SETUP_PATH=api-docs
# 标题及描述
SWAGGER_UI_TITLE=氚云3.0 BFF文档
SWAGGER_UI_TITLE_DESC=一点寒芒先到,随后枪出如龙
# API版本
SWAGGER_API_VERSION=1.0
# Swagger Api Prefix
SWAGGER_ENDPOINT_PREFIX=api/v1


# ------- 开发模式相关 ---------------------
NODE_ENV=development

用工厂函数组装配置!

import { registerAs } from '@nestjs/config';
export interface EnvSwaggerOptions {
  title: string;
  setupUrl: string;
  desc?: string;
  prefix: string;
  version: string;
}
export default registerAs(
  'EnvSwaggerOptions',
  (): EnvSwaggerOptions => ({
    title: process.env.SWAGGER_UI_TITLE, // swagger标题
    desc: process.env.SWAGGER_UI_TITLE_DESC, // swagger描述
    version: process.env.SWAGGER_API_VERSION, // swagger api 版本,自定义的
    setupUrl: process.env.SWAGGER_SETUP_PATH, // UI文档路径
    prefix: process.env.SWAGGER_ENDPOINT_PREFIX, // 接口聚合前缀,在nest用全局prefix,但是丢给swagger定义也不冲突
  }),
);

代码入口(main.ts)

熟悉的味道,还是把一些配置抽里成环境变量,
外部维护,通过注册中心使用~~
老规矩,从typescript声明入手~~~

import { INestApplication } from '@nestjs/common';
import { OpenAPIObject, SwaggerCustomOptions, SwaggerDocumentOptions } from './interfaces';
export declare class SwaggerModule {
    static createDocument(app: INestApplication, config: Omit<OpenAPIObject, 'paths'>, options?: SwaggerDocumentOptions): OpenAPIObject;
    static setup(path: string, app: INestApplication, document: OpenAPIObject, options?: SwaggerCustomOptions): void;
    private static setupExpress;
    private static setupFastify;
}

import { OpenAPIObject } from './interfaces';
import { ExternalDocumentationObject, SecuritySchemeObject, ServerVariableObject } from './interfaces/open-api-spec.interface';
export declare class DocumentBuilder {
    private readonly logger;
    private readonly document;
    setTitle(title: string): this;  // 设置swagger ui标题
    setDescription(description: string): this; // 设置swagger ui描述
    setVersion(version: string): this; // 设置swagger ui版本
    setTermsOfService(termsOfService: string): this; // 设置条例链接,可以单纯理解为一个外链
    setContact(name: string, url: string, email: string): this; // 联系信息
    setLicense(name: string, url: string): this; // 采用的协议,比如MIT等等
  	// 若是用到了外部nginx这类接口,这个可以拼接请求域
    addServer(url: string, description?: string, variables?: Record<string, ServerVariableObject>): this; 
    setExternalDoc(description: string, url: string): this; // 设置外部文档链接
    setBasePath(path: string): this; // 可以理解为聚合前缀,在nest有自己的api可以用,可以忽略设置这个
    addTag(name: string, description?: string, externalDocs?: ExternalDocumentationObject): this; // 添加swagger分类
    addSecurity(name: string, options: SecuritySchemeObject): this; // 以下都是鉴权安全性相关的
    addSecurityRequirements(name: string, requirements?: string[]): this; // ...
    addBearerAuth(options?: SecuritySchemeObject, name?: string): this;// Bearer 认证
    addOAuth2(options?: SecuritySchemeObject, name?: string): this;// OAuth2 认证
    addApiKey(options?: SecuritySchemeObject, name?: string): this;// 
    addBasicAuth(options?: SecuritySchemeObject, name?: string): this;// 基础认证
    addCookieAuth(cookieName?: string, options?: SecuritySchemeObject, securityName?: string): this; // Cookie 认证
    build(): Omit<OpenAPIObject, 'components' | 'paths'>; // 读取设置好的配置构建出swagger的集中化配置
}

export interface SwaggerDocumentOptions {
    include?: Function[]; // 手动指定包含的模块
    extraModels?: Function[]; // 额外的model定义需和上面的关联,也就是存在include里面的
    ignoreGlobalPrefix?: boolean; // 这个设置为true,会忽略setGlobalPrefix的设置
    deepScanRoutes?: boolean; // 开启这个,只要是import的都会追加的索引的路由
    // 操作id,可以通过这个工厂函数来改变id的定义(接口请求生成)
 		// 默认走的是@default () => controllerKey_methodKey, 模块_方法
    operationIdFactory?: (controllerKey: string, methodKey: string) => string; 
}


export interface SwaggerCustomOptions {
    explorer?: boolean; // 开了没啥效果
    swaggerOptions?: any; // swagger ui的配置
    customCss?: string; // 自定义css
    customCssUrl?: string; // 自定义css 链接
    customJs?: string; // 同上,js
    customfavIcon?: string;// 同上,小图标
    swaggerUrl?: string; // swagger链接设置
    customSiteTitle?: string; // 自定义网站标题
    validatorUrl?: string; // 远程校验url,一般用不到
    url?: string;// 指向API定义的URL(通常是swagger。json或swagger.yaml)。如果使用url或规范,将被忽略。
    urls?: Record<'url' | 'name', string>[];// 没用过
}

import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { EnvSwaggerOptions } from './config/env/swagger.config';
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,
  });
  const configService = app.get(ConfigService);
  const swaggerOptions = configService.get<EnvSwaggerOptions>(
    'EnvSwaggerOptions',
  );

  // 设置全局请求访问前缀
  app.setGlobalPrefix(swaggerOptions.prefix);

  const options = new DocumentBuilder()
    .setExternalDoc('xxxxx前端文档 ','htxxxxx')
    .setTitle(swaggerOptions.title)
    .setDescription(swaggerOptions.desc)
    .setVersion(swaggerOptions.version)
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup(swaggerOptions.setupUrl, app, document, {
    customSiteTitle: swaggerOptions.title,
    swaggerOptions: {
      explorer: true,
      docExpansion: 'list',
      filter: true,
      showRequestDuration: true,
      syntaxHighlight:{
        active:true,
        theme:"tomorrow-night"
      }
    }
  });
  await app.listen(configService.get('SERVE_LISTENER_PORT'));
}
bootstrap()

注解介绍

用于DTO的注解

也就是用来生成modal(字段的解释)

  • @ApiProperty () : @ApiProperty({ required:false}) 就等同于下面,
  • @ApiPropertyOptional() : 这个是上个基础上把必填变成选填

我们看下typescript的声明~~~

// 相当直观,大多数类型的及区间的限制都能一目了然
export interface SchemaObject {
    nullable?: boolean;
    discriminator?: DiscriminatorObject;
    readOnly?: boolean;
    writeOnly?: boolean;
    xml?: XmlObject;
    externalDocs?: ExternalDocumentationObject;
    example?: any;
    examples?: any[];
    deprecated?: boolean;
    type?: string;
    allOf?: (SchemaObject | ReferenceObject)[];
    oneOf?: (SchemaObject | ReferenceObject)[];
    anyOf?: (SchemaObject | ReferenceObject)[];
    not?: SchemaObject | ReferenceObject;
    items?: SchemaObject | ReferenceObject;
    properties?: Record<string, SchemaObject | ReferenceObject>;
    additionalProperties?: SchemaObject | ReferenceObject | boolean;
    description?: string;
    format?: string;
    default?: any;
    title?: string;
    multipleOf?: number;
    maximum?: number;
    exclusiveMaximum?: boolean;
    minimum?: number;
    exclusiveMinimum?: boolean;
    maxLength?: number;
    minLength?: number;
    pattern?: string;
    maxItems?: number;
    minItems?: number;
    uniqueItems?: boolean;
    maxProperties?: number;
    minProperties?: number;
    required?: string[];
    enum?: any[];
}
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

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

export class CreateAppDto {
  @ApiPropertyOptional({
    description: '姓名',
  })
  readonly name?: string;

  @ApiProperty({ description: '年龄', minimum: 0, maximum: 130 })
  readonly age: number;


  @ApiPropertyOptional({
    description: '爱好',
  })
  readonly hobit: string;
  
  
  @ApiProperty({ enum: ['Boss', 'Admin', 'User']})
  role: UserRole;

}


用于业务逻辑的

这个就好多了,有具体到接口回来,异常等等;
常规Response用ApiResponse就能满足很多场景,
schame,状态码都能定义~~~
具体可以跳进去看typescript,我们举个栗子!

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;
  }
}

总结

到此,基本可以边写代码及文档输出了,
只要注解用的好,swagger都能如期输出!

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