专栏首页图雀社区Nest.js 从零到壹系列(七):讨厌写文档,Swagger UI 了解一下?

Nest.js 从零到壹系列(七):讨厌写文档,Swagger UI 了解一下?

本文由图雀社区认证作者 布拉德特皮 写作而成,点击阅读原文查看作者掘金链接,感谢作者的优质输出,让我们的技术世界变得更加美好?

前言

上一篇介绍了如何使用寥寥几行代码就实现 RBAC 0,解决了权限管理的痛点,这篇将解决另一个痛点:写文档。

上家公司在恒大的时候,项目的后端文档使用 Swagger UI 来展示,这是一个遵循 RESTful API 的、 可以互动的文档,所见即所得。

然后进入了目前的公司,接口文档是用 Markdown 写的,并保存在 SVN 上,每次接口修改,都要更新文档,并同步到 SVN,然后前端再拉下来更新。

这些都还好,之前还有直接丢个 .doc 文档过来的。。。。

以前我总吐槽后端太懒,文档都不愿更新,直到自己写后端时,嗯,真香。。。于是,为了不耽误摸鱼时间,寻找一个趁手的文档工具,就提上日程了。

GitHub 项目地址[1],欢迎各位大佬 Star。

什么是 RESTful API

怎样用通俗的语言解释 REST,以及 RESTful ? - 覃超的回答 - 知乎[2]

Swagger 之旅

初始化 Swagger

$ yarn add @nestjs/swagger swagger-ui-express -S  

安装完依赖包后,只需要在 main.ts 中引入,并设置一些基本信息即可:

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
import { logger } from './middleware/logger.middleware';
import { TransformInterceptor } from './interceptor/transform.interceptor';
import { HttpExceptionFilter } from './filter/http-exception.filter';
import { AllExceptionsFilter } from './filter/any-exception.filter';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(express.json()); // For parsing application/json
  app.use(express.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
  // 监听所有的请求路由,并打印日志
  app.use(logger);
  // 使用拦截器打印出参
  app.useGlobalInterceptors(new TransformInterceptor());
  app.setGlobalPrefix('nest-zero-to-one');
  app.useGlobalFilters(new AllExceptionsFilter());
  app.useGlobalFilters(new HttpExceptionFilter());
  // 配置 Swagger
  const options = new DocumentBuilder()
    .setTitle('Nest zero to one')
    .setDescription('The nest-zero-to-one API description')
    .setVersion('1.0')
    .addTag('test')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api-doc', app, document);

  await app.listen(3000);
}
bootstrap();


接下来,我们访问 localhost:3000/api-doc/#/ (假设你的端口是 3000),不出意外,会看到下图:

这就是 Swagger UI,页面列出了我们之前写的 RouterDTO(即图中的 Schemas)

映射 DTO

点开 RegisterInfoDTO,发现里面是空的,接下来,我们配置一下参数信息,在 user.dto.ts 中引入 ApiProperty,然后添加到之前的 class-validator 上:

// src/logical/user/user.dto.ts
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class RegisterInfoDTO {
  @ApiProperty()
  @IsNotEmpty({ message: '用户名不能为空' })
  readonly accountName: string;
  @ApiProperty()
  @IsNotEmpty({ message: '真实姓名不能为空' })
  @IsString({ message: '真实姓名必须是 String 类型' })
  readonly realName: string;
  @ApiProperty()
  @IsNotEmpty({ message: '密码不能为空' })
  readonly password: string;
  @ApiProperty()
  @IsNotEmpty({ message: '重复密码不能为空' })
  readonly repassword: string;
  @ApiProperty()
  @IsNotEmpty({ message: '手机号不能为空' })
  @IsNumber()
  readonly mobile: number;
  @ApiProperty()
  readonly role?: string | number;
}

保存,刷新页面(该页面没有热加载功能),再看看效果:

看到已经有了字段信息了,但是我们的 role 字段是【可选】的,而文档中是【必填】的,接下来再完善一下描述:

// src/logical/user/user.dto.ts
  @ApiProperty({
    required: false,
    description: '[用户角色]: 0-超级管理员 | 1-管理员 | 2-开发&测试&运营 | 3-普通用户(只能查看)',
  })
  readonly role?: number | string;

其实,我们可以使用 ApiPropertyOptional 装饰器来表示【可选】参数,这样就不用频繁写 required: false 了:

// src/logical/user/user.dto.ts
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class RegisterInfoDTO {
  ...
  @ApiPropertyOptional({
    description: '[用户角色]: 0-超级管理员 | 1-管理员 | 2-开发&测试&运营 | 3-普通用户(只能查看)',
  })
  readonly role?: number | string;
}

接口标签分类

通过前面的截图可以看到,所有的接口都在 Default 栏目下,接口多了之后,就很不方便查找了。

我们可以根据 Controller 来分类,添加装饰器 @ApiTags 即可:

// src/logical/user/user.controller.ts
import { Controller, Post, Body, UseGuards, UsePipes } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from '../auth/auth.service';
import { UserService } from './user.service';
import { ValidationPipe } from '../../pipe/validation.pipe';
import { RegisterInfoDTO } from './user.dto';
import { ApiTags } from '@nestjs/swagger';

@ApiTags('user') // 添加 接口标签 装饰器
@Controller('user')
export class UserController {
  constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}

  // JWT验证 - Step 1: 用户请求登录
  @Post('login')
  async login(@Body() loginParmas: any) {
    ...
  }

  @UseGuards(AuthGuard('jwt'))
  @UsePipes(new ValidationPipe())
  @Post('register')
  async register(@Body() body: RegisterInfoDTO) {
    return await this.usersService.register(body);
  }
}

保存再刷新一下页面,看到用户相关的都在一个栏目下了:

在 Swagger 中登录

接下来,我们测试一下注册接口的请求,先编辑参数,然后点击 Execute:

然后看一下返回参数:

看到返回的是 401 未登录。

那么,如何在 Swagger 中登录呢?

我们先完善登录接口的 DTO:

// src/logical/user/user.dto.ts
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class LoginDTO {
  @ApiProperty()
  @IsNotEmpty({ message: '用户名不能为空' })
  readonly username: string;
  @ApiProperty()
  @IsNotEmpty({ message: '密码不能为空' })
  readonly password: string;
}

export class RegisterInfoDTO {
  ...
}

然后在 main.ts 中加上 addBearerAuth() 方法,启用承载授权

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';
import { logger } from './middleware/logger.middleware';
import { TransformInterceptor } from './interceptor/transform.interceptor';
import { HttpExceptionFilter } from './filter/http-exception.filter';
import { AllExceptionsFilter } from './filter/any-exception.filter';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  ...
  // 配置 Swagger
  const options = new DocumentBuilder()
    .addBearerAuth() // 开启 BearerAuth 授权认证
    .setTitle('Nest zero to one')
    .setDescription('The nest-zero-to-one API description')
    .setVersion('1.0')
    .addTag('test')
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('api-doc', app, document);

  await app.listen(3000);
}
bootstrap();

然后只需在 Controller 中添加 @ApiBearerAuth() 装饰器即可,顺便把登录的 DTO 也加上:

// src/logical/user/user.controller.ts
import { Controller, Post, Body, UseGuards, UsePipes } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from '../auth/auth.service';
import { UserService } from './user.service';
import { ValidationPipe } from '../../pipe/validation.pipe';
import { LoginDTO, RegisterInfoDTO } from './user.dto';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';

@ApiBearerAuth() // Swagger 的 JWT 验证
@ApiTags('user')
@Controller('user')
export class UserController {
  constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}

  // JWT 验证 - Step 1: 用户请求登录
  @Post('login')
  async login(@Body() loginParmas: LoginDTO) {
    // console.log('JWT验证 - Step 1: 用户请求登录');
    const authResult = await this.authService.validateUser(loginParmas.username, loginParmas.password);
    switch (authResult.code) {
      case 1:
        return this.authService.certificate(authResult.user);
      case 2:
        return {
          code: 600,
          msg: `账号或密码不正确`,
        };
      default:
        return {
          code: 600,
          msg: `查无此人`,
        };
    }
  }

  @UseGuards(AuthGuard('jwt'))
  @UsePipes(new ValidationPipe())
  @Post('register')
  async register(@Body() body: RegisterInfoDTO) {
    return await this.usersService.register(body);
  }
}

然后,我们去页面中登录:

Responses body 中的 token 复制出来,然后将页面拖到顶部,点击右上角那个带锁的按钮:

将 token 复制到弹窗的输入框,点击 Authorize,即可授权成功:

注意:这里显示的授权 Value 是密文,也就是,如果你复制错了,或者 token 过期了,也不会有任何提示。

现在,我们再重新请求一下注册接口:

成功!

示例参数

前面登录的时候,需要手动输入用户名、密码,那么有没有可能,事先写好,这样前端来看文档的时候,直接用默认账号登录就行了呢?

我们先给 DTO 加点料:

// src/logical/user/user.dto.ts
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

// @ApiExtraModels(LoginDTO)
export class LoginDTO {
  @ApiProperty({ description: '用户名', example: 'koa2', })
  @IsNotEmpty({ message: '用户名不能为空' })
  readonly username: string;
  @ApiProperty({ description: '密码', example: 'a123456' })
  @IsNotEmpty({ message: '密码不能为空' })
  readonly password: string;
}

export class RegisterInfoDTO {
  ...
}

然后,去 Controller 中引入 ApiBody, 并用来装饰接口,type 直接指定 LoginDTO 即可:

// src/logical/user/user.controller.ts
import { Controller, Post, Body, UseGuards, UsePipes } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from '../auth/auth.service';
import { UserService } from './user.service';
import { ValidationPipe } from '../../pipe/validation.pipe';
import { LoginDTO, RegisterInfoDTO } from './user.dto';
import { ApiTags, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
@ApiBearerAuth()
@ApiTags('user')
@Controller('user')
export class UserController {
  constructor(private readonly authService: AuthService, private readonly usersService: UserService) {}

  // JWT验证 - Step 1: 用户请求登录
  @Post('login')
  @ApiBody({
    description: '用户登录',
    type: LoginDTO,
  })
  async login(@Body() loginParmas: LoginDTO) {
    ...
  }

  @UseGuards(AuthGuard('jwt'))
  @UsePipes(new ValidationPipe())
  @Post('register')
  async register(@Body() body: RegisterInfoDTO) {
    return await this.usersService.register(body);
  }
}

保存代码,再刷新一下页面:

并且点击 Schema 的时候,还能看到 DTO 详情:

再点击 try it out 按钮的时候,就会自动使用默认参数了:

总结

本篇介绍了如何使用 Swagger 自动生成可互动的文档。

可以看到,我们只需在写代码的时候,加一些装饰器,并配置一些属性,就可以在 Swagger UI 中生成文档,并且这个文档是根据代码,实时更新的。查看文档,只需访问链接即可,不用再传来传去了,你好我好大家好。

本篇只是抛砖引玉, Swagger UI 还有很多可配置的玩法,比如数组应该怎么写,枚举应该怎么弄,如何设置请求头等等,因为篇幅原因,就不在这里展开了。有兴趣的同学,可以自行去官网了解~

本篇收录于NestJS 实战教程[3],更多文章敬请关注。

参考资料

[1]

GitHub 项目地址: https://github.com/SephirothKid/nest-zero-to-one

[2]

怎样用通俗的语言解释 REST,以及 RESTful ? - 覃超的回答 - 知乎: https://www.zhihu.com/question/28557115/answer/48094438

[3]

NestJS 实战教程: https://juejin.im/collection/5e893a1b6fb9a04d65a15400

[4]

Nest 官网 - OpenAPI (Swagger): https://docs.nestjs.com/recipes/swagger

[5]

Swagger - OpenAPI Specification: https://swagger.io/specification/

[6]

Swagger UI tutorial: https://idratherbewriting.com/learnapidoc/pubapis_swagger.html#make-a-request

● Nest.js 从零到壹系列(二):数据库的连接● Nest.js 从零到壹系列(四):使用中间件、拦截器、过滤器打造日志系统● Nest.js 从零到壹系列(六):用 15 行代码实现 RBAC 0

·END·

本文分享自微信公众号 - 图雀社区(tuture-dev),作者:布拉德特皮

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • JavaScript 测试系列实战(三):使用 Mock 模拟模块并处理组件交互

    在之前的两篇教程中,我们学会了如何去测试最简单的 React 组件。在实际开发中,我们的组件经常需要从外部 API 获取数据,并且组件的交互逻辑也往往更复杂。在...

    一只图雀
  • Nest.js 从零到壹系列(三):使用 JWT 实现单点登录

    上一篇介绍了如何使用 Sequelize 连接 MySQL,接下来,在原来代码的基础上进行扩展,实现用户的注册和登录功能。

    一只图雀
  • Nest.js 从零到壹系列(一):项目创建&路由设置&模块

    本系列将以前端的视角进行书写,分享自己的踩坑经历。教程主要面向前端或者毫无后端经验,但是又想尝试 Node.js 的读者,当然,也欢迎后端大佬斧正。

    一只图雀
  • 浅谈Python Django框架

    Python下有多款不同的 Web 框架,Django是最有代表性的一种。许多成功的网站和APP都基于Django。

    小小科
  • 浅谈Python Django框架

    Python下有多款不同的 Web 框架,Django是最有代表性的一种。许多成功的网站和APP都基于Django。

    马哥linux运维
  • 简单4步用FLASK/Django部署你的Pyecharts项目

    因为如果只是单纯的使用pyecharts,我们展示的时候只能将我们的图表生成为静态文件,虽然也在也可以使用iframe等语句嵌入前端页面但是并不方便前后端数据交...

    刘早起
  • 模块和包

    1.无论是import形式还是from...import形式,凡是在导入语句中(而不是在使用时)遇到带点的,都要第一时间提高警觉:这是关于包才有的导入语法

    py3study
  • python3--模块和包,软件开发规范

    在第一次导入某个模块时(比如my_module),会检查该模块是否已经被加载到内存中(执行文件的名称空间对应的内存),如果有则直接引用,如果没有,解释器则会查找...

    py3study
  • 使用Moment.js处理时间戳转化为时间年月

    Moment.js 是一个 JavaScript 日期处理类库(处理时间格式化的npm包),用于解析、检验、操作、以及显示日期,在新公司的项目中,大量使用Mom...

    祈澈菇凉
  • 7 Papers & Radios | 史上最大AI模型GPT-3上线;Transformer跨界做目标检测

    论文 1:Knowledge Graph Embedding for Link Prediction: A Comparative Analysis

    机器之心

扫码关注云+社区

领取腾讯云代金券