这篇填下面两个坑:
- 接口包装:将return的数据按约定好的结构进行返回
1 2 3 4 5
| { "code": 0, "msg": "success", "data": "Hello world" }
|
- 异常处理:按约定好的结构,返回业务状态码和异常messge
1 2 3 4
| { "code": 4002, "msg": "手机号长度错误" }
|
业务状态码
常见的第三方接口,会带上自己定义的业务状态码,举一个简单的例子
1 2 3 4 5 6 7 8 9 10
| export enum ApiCode { TIMEOUT = -1, SUCCESS = 0,
BUSINESS_ERROR = 4001, PARAMS_ERROR = 4002, SIGN_ERROR = 4003, TOKEN_ERROR = 4004, }
|
接口包装
1 2 3 4
| @Get() async getString(): Promise<string> { return 'Hello world' }
|
默认情况下,客户端拿到的就只有'Hello world'
字符串,但是我们不可能在每个controller里都写一遍包装的逻辑再return掉,这时候就需要拦截器替我们自动包装了。
自定义拦截器需要实现NestInterceptor接口,定义如下:
1 2 3
| export interface NestInterceptor<T = any, R = any> { intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> | Promise<Observable<R>>; }
|
ExecutionContext
接口定义:
1 2 3 4
| export interface ExecutionContext extends ArgumentsHost { getClass<T = any>(): Type<T>; getHandler(): Function; }
|
ExecutionContext
继承自ArgumentsHost
, ArgumentsHost
则是对上下文的包装(其实是个数组):如果是HTTP应用,那么就是[request, response]
;如果是Web Socket应用,那么就是[client, data]
。
ExecutionContext
则添加了两种方法,getHandler
获取的是处理响应的Handler(比如上文的getString
);getClass
获取的是Hanlder所属的Controller类(不是实例)
再看一下CallHandler
的接口定义
1 2 3
| export interface CallHandler<T = any> { handle(): Observable<T>; }
|
它的作用很简单,调用handle
时返回订阅的Observable
,Observable
是RxJS
相关概念,不展开描述,它的核心是流和操作符(用起来其实很方便,但是理解真的很抽象)
回到实际场景,新建拦截器providers/interceptor/api.interceptor.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { ApiCode } from '../../enum/api-code.enum'
interface Response<T> { code: number msg: string data: T }
@Injectable() export class ApiInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept( context: ExecutionContext, next: CallHandler ): Observable<Response<T>> { return next.handle().pipe( map(data => { return { code: ApiCode.SUCCESS, msg: 'success', data } }) ) } }
|
这样子一个简单的接口包装就完成了,由于我们对每一个响应都需要包装,就把它注册为全局拦截器:
1 2 3 4 5 6 7 8 9 10
| import { Module } from '@nestjs/common' import { APP_INTERCEPTOR } from '@nestjs/core'
@Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: ApiInterceptor } ] }) export class ApplicationModule {}
|
完成。
异常包装
上面针对的是正常返回(return)的情况,在实际业务时会有各类异常情况出现,按照最直接的方法来的话(不含上述拦截器):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Get(':id') async getUser(@Res() res, @Param('id', new ParseIntPipe()) id) { if(isNaN(id)) { return res.status(HttpStatus.OK).send({ code: 4002, msg: 'id不合法' }) }
return res.status(HttpStatus.OK).send({ code: 0, msg: 'success', data: { name: 'a', age: 18 } }) }
|
这种写法会带来耦合度高、代码冗余等问题,且和刚才写的接口包装拦截器有冲突,那么这些异常该如何优雅地进行返回呢?
Nest会对应用抛出的异常进行捕获,生成一个JSON返回,比如访问一个不存在的path:
1 2 3 4 5
| { "statusCode": 404, "error": "Not Found", "message": "Cannot GET /member" }
|
因此我们只需要抛出异常就行了,正常返回也可以通过拦截器进行包装:
1 2 3 4 5 6 7 8 9
| @Get(':id') async getUser(@Res() res, @Param('id', new ParseIntPipe()) id) { if(isNaN(id)) { throw new HttpException('ID不合法', HttpStatus.OK) }
return { name: 'a', age: 18 } }
|
但是这样返回的异常并不符合我们需要的结构,我们需要自定义一个异常过滤器来处理异常的返回。
在这之前,我们先自定义一个异常,把我们的业务码放进去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { HttpException, HttpStatus } from '@nestjs/common' import { ApiCode } from '../../enum/api-code.enum'
export class ApiException extends HttpException { private errorMessage: string private errorCode: ApiCode
constructor( errorMessage: string, errorCode: ApiCode, statusCode: HttpStatus ) { super(errorMessage, statusCode)
this.errorMessage = errorMessage this.errorCode = errorCode }
gerErrorCode(): ApiCode { return this.errorCode }
getErrorMessage(): string { return this.errorMessage } }
|
自定义过滤器需要实现ExceptionFilter
接口:
1 2 3
| export interface ExceptionFilter<T = any> { catch(exception: T, host: ArgumentsHost): any; }
|
exception
是捕获到的异常,host
是ArgumentsHost
实例,上文有提到。
然后写我们需要的自定义过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common' import { ApiException } from '../exception/api.exception'
@Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception, host: ArgumentsHost): void { const ctx = host.switchToHttp() const response = ctx.getResponse() const request = ctx.getRequest() const status = exception.getStatus()
if (exception instanceof ApiException) { response.status(status).json({ msg: (exception as ApiException).getErrorMessage(), code: (exception as ApiException).gerErrorCode(), path: request.url }) } else { response.status(status).json((exception as HttpException).getResponse()) } } }
|
最后把过滤器也注册到全局上:
1 2 3 4 5 6 7 8 9 10 11
| import { Module } from '@nestjs/common' import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'
@Module({ providers: [ { provide: APP_FILTER, useClass: HttpExceptionFilter }, { provide: APP_INTERCEPTOR, useClass: ApiInterceptor } ] }) export class ApplicationModule {}
|
使用方法:
1 2 3 4 5 6 7 8 9
| @Get(':id') async getUser(@Res() res, @Param('id', new ParseIntPipe()) id): Promise<User> { if(isNaN(id)) { throw new ApiException('ID不合法', ApiCode.PARAMS_ERROR, 200) }
return { name: 'a', age: 18 } }
|
HTTP状态码返回200还是4xx?
方案一:200 + 业务码 + message
方案二(RESTful):原生http status code + message
别问,问就是看约定,本前端都能接受…
(据说某些浏览器对部分状态码有奇特处理)