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