阅读 1019
NestJS 搭建博客系统(四)— 使用拦截器、异常过滤器实现统一返回格式

NestJS 搭建博客系统(四)— 使用拦截器、异常过滤器实现统一返回格式

NestJS 搭建博客系统(四)— 使用拦截器、异常过滤器实现统一返回格式

前言

上一个章节我们实现了数据持久话,至此,我们已经拥有一个能用的curd模块了,在真实项目中,为了对接方便以及友好提示,服务端会使用统一的返回格式包装数据。

返回结构体

在工作中接触到一种比较舒服的接口格式,这里推荐一下,有更好的实践可以分享一下。

// 成功返回
{
  code: 200,
  data: {
    // 详情类
    info: { 
      // 返回数据
    },

    // 列表类
    list: [],

    pagination: {
      total: 100,
      pageSize: 10,
      pages: 10,
      page: 1,
    },
  },
  message: "请求成功"
},

// 失败返回
{
  code: 400,
  message: "查询失败",
}

复制代码

code 使用 http code 基本可以满足。 info 用于承载详情类,多个 info 使用不同的前缀,如 userInfo,articleInfo 。 list 用于列表,多个列表参考 info。 pagination 用于承载分页信息。

实现

处理请求成功

根据 Nest 的生命周期以及文档介绍,我们可以在请求后的拦截器中对成功的请求进行拦截包装。

创建 拦截器

nest g in interceptor/transform
复制代码

修改拦截器代码

// src/interception/transform.interception.ts

import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(
        map(data => ({ 
          code: 200,
          data,
          message: 'success'
        }))
      )
  }
}
复制代码

在 main 中使用全局拦截器

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-execption.filter';
import { TransformInterceptor } from './interceptor/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalInterceptors(new TransformInterceptor())

  await app.listen(3000);
}
bootstrap();
复制代码

修改一下 atricle.service 的 getOne 方法

// src/modules/article/article.service.ts

  async getOne(
    idDto: IdDTO  
  ) {
    const { id } = idDto
    const articleDetial = await this.articleRepository
      .createQueryBuilder('article')
      .where('article.id = :id', { id })
      .getOne()

    if (!articleDetial) {
      throw new NotFoundException('找不到文章')
    }
    
    const result = {
      info: infoarticleDetial,
    }

    return result
  }
复制代码

请求一下 /atricle/info?id=1

可以看到返回信息符合我们的预期

{
  "code": 200,
  "data": {
    "info": {
      "id": 1,
      "createTime": "2021-06-29T02:48:28.623Z",
      "updateTime": "2021-06-29T02:48:28.623Z",
      "isDelete": false,
      "version": 1,
      "title": "标题1",
      "description": "描述1",
      "content": "详情1"
    }
  },
  "message": "success"
}
复制代码

处理请求失败

失败返回我们可以直接使用Nest提供的基础异常类,但是格式和我们想要的不一样,所以我们这里使用 异常过滤器处理一下。

NestJS 提供了基础的 HTTP 异常类

含义状态码
BadRequestException服务器不理解客户端的请求,未做任何处理400
UnauthorizedException用户未提供身份验证凭据,或者没有通过身份验证401
NotFoundException所请求的资源不存在,或不可用404
ForbiddenException用户通过了身份验证,但是不具有访问资源所需的权限403
NotAcceptableException不可接受406
RequestTimeoutException请求超时408
ConflictException冲突409
GoneException所请求的资源已从这个地址转移,不再可用410
PayloadTooLargeException负载过大413
UnsupportedMediaTypeException客户端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是客户端要求返回 XML 格式。415
UnprocessableException客户端上传的附件无法处理,导致请求失败422
InternalServerErrorException客户端请求有效,服务器处理时发生了意外500
NotImplementedException未实现501
BadGatewayException坏网关502
ServiceUnavailableException服务器无法处理请求,一般用于网站维护状态503
GatewayTimeoutException网关超时504

博客系统涉及到的业务相对还是比较少的,所以直接采用Nest提供的方式,然后统一一下返回格式即可

创建拦截器 nest g f filters/httpExecption

修改 filters/http-execption.filters.ts

// src/filters/http-execption.filters.ts

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
import { execPath } from 'process';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();
    const message = exception.message

    response
      .status(status)
      .json({
        code: status,
        message,
      });
  }
}
复制代码

在 main.ts 中全局使用异常过滤器

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filters/http-execption.filter';
import { TransformInterceptor } from './interceptor/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalInterceptors(new TransformInterceptor())
  app.useGlobalFilters(new HttpExceptionFilter())

  await app.listen(3000);
}
bootstrap();
复制代码

请求一下 /article/info?id=10000

{
    "code": 404,
    "message": "找不到文章"
}
复制代码

至此,我们的格式化已经基本配置完毕,接下来改写一下 article 其他方法

// src/modules/article/article.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { ArticleCreateDTO } from './dto/article-create.dto';
import { ArticleEditDTO } from './dto/article-edit.dto';
import { IdDTO } from './dto/id.dto';
import { ListDTO } from './dto/list.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Article } from './entity/article.entity';
import { getPagination } from 'src/utils';

@Injectable()
export class ArticleService {  
  constructor(
    @InjectRepository(Article)
    private readonly articleRepository: Repository<Article>,
  ) {}

  /**
   * 
   * @param listDTO 
   * @returns 
   */
  async getMore(
    listDTO: ListDTO,
  ) {
		const { page = 1, pageSize = 10 } = listDTO
    const getList = this.articleRepository
      .createQueryBuilder('article')
      .where({ isDelete: false })
      .select([
        'article.id',
        'article.title', 
        'article.description',
        'article.createTime',
        'article.updateTime',
      ])
      .skip((page - 1) * pageSize)
      .take(pageSize)
      .getManyAndCount()

    const [list, total] = await getList
    const pagination = getPagination(total, pageSize, page)

    return {
      list,
      pagination,
    }
  }

  /**
   * 
   * @param idDto 
   * @returns 
   */
  async getOne(
    idDto: IdDTO  
  ) {
    const { id } = idDto
		const articleDetial = await this.articleRepository
      .createQueryBuilder('article')
      .where('article.id = :id', { id })
      .getOne()

    if (!articleDetial) {
      throw new NotFoundException('找不到文章')
    }

		return {
      info: articleDetial
    }
  }

  /**
   * 
   * @param articleCreateDTO 
   * @returns 
   */
  async create(
    articleCreateDTO: ArticleCreateDTO
  ){
    const article = new Article();
    article.title = articleCreateDTO.title
    article.description = articleCreateDTO.description
    article.content = articleCreateDTO.content
    const result = await this.articleRepository.save(article);
    
    return {
      info: result
    }
  }

  /**
   * 
   * @param articleEditDTO 
   * @returns 
   */
  async update(
    articleEditDTO: ArticleEditDTO
  ) {
    const { id } = articleEditDTO
    let articleToUpdate = await this.articleRepository.findOne({ id })
    articleToUpdate.title = articleEditDTO.title
    articleToUpdate.description = articleEditDTO.description
    articleToUpdate.content = articleEditDTO.content
    const result = await this.articleRepository.save(articleToUpdate)

    return {
      info: result,
    }
  }
  
  /**
   * 
   * @param idDTO 
   * @returns 
   */
  async delete (
    idDTO: IdDTO,
  ) {
    const { id } = idDTO
    let articleToUpdate = await this.articleRepository.findOne({ id })
    articleToUpdate.isDelete = true
    const result = await this.articleRepository.save(articleToUpdate)
    
    return {
      info: result
    }
  }

}
复制代码
// src/utils/index.ts

/**
 * 计算分页
 * @param total 
 * @param pageSize 
 * @param page 
 * @returns 
 */
export const getPagination = (
  total: number, 
  pageSize: number, 
  page: number) => {
  const pages = Math.ceil(total / pageSize)
  return {
    total,
    page,
    pageSize,
    pages,
  }
}

复制代码

参考

系列

文章分类
前端
文章标签