4

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

 3 years ago
source link: https://tuture.co/2020/05/12/@4675l54tY/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
7.jpg

布拉德特皮

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

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

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

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

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

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

什么是 RESTful API

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

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 实战教程,更多文章敬请关注。

参考资料:

Nest 官网 - OpenAPI (Swagger)

Swagger - OpenAPI Specification

Swagger UI tutorial


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK