Typescript自定義異常類和枚舉業務狀態碼

前言

最近在Midwayjs框架上搭建服務端項目,一個請求進來,執行鏈比較長,中間一旦出現校驗不通過,需要進行異常處理,如果要在業務代碼中進行異常處理十分麻煩且難以維護,從而引申出如何優雅地處理異常。

最簡單的方式就是需要處理異常時,直接拋出異常,在全局異常處理中間件中進行捕獲、處理、返回給前端。

我的期望是在拋出異常的同時,可以傳遞一些參數,比如業務狀態碼、http請求狀態碼、錯誤明細等參數。顯然直接throw new Error(msg:string)是沒辦法做到的。所以我們需要自定義異常類,繼承Error類,構造器中允許傳入各種參數。

http請求狀態與業務狀態碼

由於早期某些運營商會攔截非200的http請求狀態碼,導致有一批開發者http請求狀態碼只用200,然後定義了一大堆業務狀態碼在body中進行傳遞。
萬年姨媽貼之一

但其實我覺得自定義業務狀態碼是有必要的。至少我接觸的其他比較規範的項目都有自己的一套業務狀態碼。http請求狀態碼主要表示的請求的處理狀態,也沒辦法跟業務掛鉤,但現在也沒必要全部只用200,以下是我挑出來比較常用的http請求狀態碼:

  1. 200 (成功)服務器已成功處理了請求。
  2. 401(未授權)請求要求身份驗證。一旦出現這個狀態,需要重新登陸
  3. 403 (無權限)請求的資源不允許訪問。比如說,你使用普通用戶的 Token 去請求管理員才能訪問的資源。
  4. 404(未找到)服務器找不到請求的網頁。
  5. 408(請求超時)服務器等候請求時發生超時 。
  6. 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只是編譯時檢查語法,編譯後就會把這些語法檢查去掉)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章