前言
最近在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只是編譯時檢查語法,編譯後就會把這些語法檢查去掉)。