Nest.js 從零到壹系列(七):討厭寫文檔,Swagger UI 瞭解一下?

前言

上一篇介紹瞭如何使用寥寥幾行代碼就實現 RBAC 0,解決了權限管理的痛點,這篇將解決另外一個痛點:寫文檔。html

上家公司在恆大的時候,項目的後端文檔使用 Swagger UI 來展現,這是一個遵循 RESTful API 的、 能夠互動的文檔,所見即所得。前端

而後進入了目前的公司,接口文檔是用 Markdown 寫的,並保存在 SVN 上,每次接口修改,都要更新文檔,並同步到 SVN,而後前端再拉下來更新。git

這些都還好,以前還有直接丟個 .doc 文檔過來的。。。。github

之前我總吐槽後端太懶,文檔都不肯更新,直到本身寫後端時,嗯,真香。。。因而,爲了避免耽誤摸魚時間,尋找一個趁手的文檔工具,就提上日程了。typescript

GitHub 項目地址,歡迎各位大佬 Star。express

什麼是 RESTful API

怎樣用通俗的語言解釋 REST,以及 RESTful ? - 覃超的回答 - 知乎json

Swagger 之旅

初始化 Swagger

$ yarn add @nestjs/swagger swagger-ui-express -S  
複製代碼

安裝完依賴包後,只須要在 main.ts 中引入,並設置一些基本信息便可:bootstrap

// 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)api

映射 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

相關文章
相關標籤/搜索