接口包装:将return的数据按约定好的结构进行返回
{
"code": 0,
"msg": "success",
"data": "Hello world"
}
异常处理:按约定好的结构,返回业务状态码和异常messge
{
"code": 4002,
"msg": "手机号长度错误"
}
业务状态码
常见的第三方接口,会带上自己定义的业务状态码,举一个简单的例子
// enums/api-code.enums.ts
export enum ApiCode {
TIMEOUT = -1, // 系统繁忙
SUCCESS = 0, // 请求成功
BUSINESS_ERROR = 4001, // 业务错误
PARAMS_ERROR = 4002, // 参数不合法
SIGN_ERROR = 4003, // 验签失败
TOKEN_ERROR = 4004, // token不合法
}
接口包装
@Get()
async getString(): Promise<string> {
return 'Hello world'
}
默认情况下,客户端拿到的就只有'Hello world'
字符串,但是我们不可能在每个controller里都写一遍包装的逻辑再return掉,这时候就需要拦截器替我们自动包装了。
自定义拦截器需要实现NestInterceptor接口,定义如下:
export interface NestInterceptor<T = any, R = any> {
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<R> | Promise<Observable<R>>;
}
ExecutionContext
接口定义:
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
的接口定义
export interface CallHandler<T = any> {
handle(): Observable<T>;
}
它的作用很简单,调用handle
时返回订阅的Observable
,Observable
是RxJS
相关概念,不展开描述,它的核心是流和操作符(用起来其实很方便,但是理解真的很抽象)
回到实际场景,新建拦截器providers/interceptor/api.interceptor.ts
// providers/interceptor/api.interceptor.ts
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
}
})
)
}
}
这样子一个简单的接口包装就完成了,由于我们对每一个响应都需要包装,就把它注册为全局拦截器:
// app.module.ts
import { Module } from '@nestjs/common'
import { APP_INTERCEPTOR } from '@nestjs/core'
@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: ApiInterceptor }
]
})
export class ApplicationModule {}
完成。
异常包装
上面针对的是正常返回(return)的情况,在实际业务时会有各类异常情况出现,按照最直接的方法来的话(不含上述拦截器):
// user.controller.ts
@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:
{
"statusCode": 404,
"error": "Not Found",
"message": "Cannot GET /member"
}
因此我们只需要抛出异常就行了,正常返回也可以通过拦截器进行包装:
// user.controller.ts
@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 }
}
但是这样返回的异常并不符合我们需要的结构,我们需要自定义一个异常过滤器来处理异常的返回。
在这之前,我们先自定义一个异常,把我们的业务码放进去:
// api.exception.ts
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
接口:
export interface ExceptionFilter<T = any> {
catch(exception: T, host: ArgumentsHost): any;
}
exception
是捕获到的异常,host
是ArgumentsHost
实例,上文有提到。
然后写我们需要的自定义过滤器:
// http-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException
} from '@nestjs/common'
import { ApiException } from '../exception/api.exception'
// 该装饰器告诉filter要捕获哪些类型异常
@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())
}
}
}
最后把过滤器也注册到全局上:
// app.module.ts
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 {}
使用方法:
// user.controller.ts
@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