前言
最近在Midwayjs框架上搭建服务端项目,一个请求进来,执行链比较长,中间一旦出现校验不通过,需要进行异常处理,如果要在业务代码中进行异常处理十分麻烦且难以维护,从而引申出如何优雅地处理异常。
最简单的方式就是需要处理异常时,直接抛出异常,在全局异常处理中间件中进行捕获、处理、返回给前端。
我的期望是在抛出异常的同时,可以传递一些参数,比如业务状态码、http请求状态码、错误明细等参数。显然直接throw new Error(msg:string)是没办法做到的。所以我们需要自定义异常类,继承Error类,构造器中允许传入各种参数。
http请求状态与业务状态码
由于早期某些运营商会拦截非200的http请求状态码,导致有一批开发者http请求状态码只用200,然后定义了一大堆业务状态码在body中进行传递。
万年姨妈贴之一
但其实我觉得自定义业务状态码是有必要的。至少我接触的其他比较规范的项目都有自己的一套业务状态码。http请求状态码主要表示的请求的处理状态,也没办法跟业务挂钩,但现在也没必要全部只用200,以下是我挑出来比较常用的http请求状态码:
- 200 (成功)服务器已成功处理了请求。
- 401(未授权)请求要求身份验证。一旦出现这个状态,需要重新登陆
- 403 (无权限)请求的资源不允许访问。比如说,你使用普通用户的 Token 去请求管理员才能访问的资源。
- 404(未找到)服务器找不到请求的网页。
- 408(请求超时)服务器等候请求时发生超时 。
- 500(服务器内部错误)服务器遇到错误,无法完成请求。
如果需要更多请求状态码可以参照以下帖子自行挑选:
关于 RESTful API 中 HTTP 状态码的定义的疑问?
至于是否只用http请求状态200,自行斟酌,没必要引战。
封装自定异常类:
先定义一个基础异常类
//业务状态码
export const BasicExceptionCode = {
PARAM_COUNT: 'BASIC0001',
PARAM_TYPE: 'BASIC0002',
PARAM_NULL: 'BASIC0003'
} as const;
export class BasicException extends Error {
protected map: Map<string, string>;
protected code: string = '';
protected msg: string | undefined = '';
protected detail: string = '';
protected httpCode: number = 500;
/**
* 构造器函数 如果子类继承了该基类,请在子类构造器中依次执行super()、this.appendMap(map)、this.check(code,detail,httpCode)
* @param code 业务状态码
* @param detail 错误明细
* @param httpCode 请求状态码
*/
constructor(code: string = 'BASIC9999', detail: string = '', httpCode = 500) {
super();
this.map = new Map([
['BASIC0001', '参数数量错误'],
['BASIC0002', '参数类型错误'],
['BASIC0003', '参数为空'],
['BASIC9999', '未知错误']
]);
//进行检查赋值
this.check(code, detail, httpCode);
}
/**
* 追加错误码Map,用于子类继承基类后,在构造器中执行super()后调用
* @param map
*/
protected appendMap(map: Map<string, string>) {
this.map = new Map<string, string>([...this.map, ...map]);
}
/**
* 检查错误码是否存在,存在提取错误状态码明细并赋值,如果不存在,则为未处理的错误。如果是子类,请在构造器中执行super()、super.setMap(map)后调用
* @param code 业务状态码
* @param detail 错误明细
* @param httpCode 请求状态码
*/
protected check(
code: string = 'BASIC9999',
detail: string = '',
httpCode = 500
) {
this.detail = detail;
this.httpCode = httpCode;
if (this.map.has(code)) {
this.code = code;
this.msg = this.map.get(code);
} else {
this.code = 'BASIC9999';
this.msg = this.map.get(code);
}
}
/**
* 获取错误状态码
*/
public getCode() {
return this.code;
}
//获取错误码中文描述
public getMsg() {
return this.msg;
}
//获取错误明细(错误明细是抛出错误时手动传入的)
public getDetail() {
return this.detail;
}
//获取请求状态码
public getHttpCode() {
return this.httpCode;
}
/**
* 转字符串
*/
public toString() {
return `httpCode:${this.httpCode},code:${this.code},msg:${this.msg},detail:${this.detail}`;
}
}
在定义一个测试类来测试我们的基础异常类
import { BasicException, BasicExceptionCode } from '../exception/basic';
class BasicTest {
constructor(...args: any[]) {
this.main(...args);
}
main(...args: any[]) {
try {
if (Object.keys(args).length !== 2) {
throw new BasicException(BasicExceptionCode.PARAM_COUNT);
}
if (args[0] === undefined || args[0] === null || args[0] === '') {
throw new Error(BasicExceptionCode.PARAM_NULL);
}
if (args[0] instanceof String === false) {
throw new BasicException(BasicExceptionCode.PARAM_TYPE);
}
throw new BasicException();
} catch (err) {
if (err instanceof BasicException) {
console.log(err.toString());
} else {
console.log(err.toString());
}
}
}
}
new BasicTest('helloWorld');
编译后执行的输出结果:
有了基础异常类还不够,我们还需要根据业务功能自定义功能异常类。
我们再定义一个继承BasicException的UserException
import { BasicException, BasicExceptionCode } from './basic';
//合并业务状态码
export const UserExceptionCode = {
USERNAME_ERR: 'USER0001',
USERNAME_LEN: 'USER0002',
PASSWORD_ERR: 'USER0003',
PASSWORD_LEN: 'USER0004',
...BasicExceptionCode
} as const;
export class UserException extends BasicException {
constructor(code: string = 'BASIC9999', detail: string = '', httpCode = 500) {
super(code, detail, httpCode);
//追加错误状态的错误描述信息
super.appendMap(
new Map([
['USER0001', '账号错误'],
['USER0002', '账号长度不符合要求'],
['USER0003', '密码错误'],
['USER0004', '密码长度不符合要求']
])
);
this.check(code, detail, httpCode);
}
}
写完之后,同样的,我们再创建一个测试类来验证我们的UserException
import { BasicException } from '../exception/basic';
import { UserException, UserExceptionCode } from '../exception/user';
class UserTest {
constructor(username: string, password: string) {
this.main(username, password);
}
main(username: string, password: string) {
try {
if (username.length < 6) {
throw new UserException(UserExceptionCode.USERNAME_LEN, username);
}
if (username !== '123456') {
throw new UserException(UserExceptionCode.USERNAME_ERR, username);
}
if (password.length < 6) {
throw new UserException(UserExceptionCode.PASSWORD_LEN, password);
}
if (password !== '123456') {
throw new UserException(UserExceptionCode.PASSWORD_ERR, password);
}
throw new UserException(UserExceptionCode.PARAM_COUNT);
} catch (err) {
if (err instanceof BasicException) {
console.log(err.toString());
} else {
console.log(err.toString());
}
}
}
}
new UserTest('123455', '123456');
编译后执行结果如下:
枚举状态码
export const UserExceptionCode = {
USERNAME_ERR: 'USER0001',
USERNAME_LEN: 'USER0002',
PASSWORD_ERR: 'USER0003',
PASSWORD_LEN: 'USER0004',
...BasicExceptionCode
} as const;
以上代码使用的是Typescript的 Const Assertions语法,并没有选择使用Typescript的枚举类型。原因是Typescript的枚举类型虽然可以定义常量,使用时可以枚举属性,但我需要为每个功能异常类追加不同的业务状态码,而Typescript枚举类型并不能很好地合并或继承,所以选择使用Const Assertions,同样可以枚举里面的属性,并且可以进行合并,而且属性值不可以修改且无法在表达式以外新增属性(最起码在开发时提示无此属性,Typecript只是编译时检查语法,编译后就会把这些语法检查去掉)。
别忘了点赞收藏支持一波