Egg 中 Controller 最佳實踐

SuperEVO發表於2018-08-20

得益於 JavaScript 加入的 decorator 特性,可以使我們跟 Java/C# 一樣,更加直觀自然的,做面向切面程式設計。而隨著 TypeScript 的成熟,型別系統也讓我們增強了信心,面對複雜的業務邏輯,也更有底氣。

egg-controller 是集合了一些在 Controller 層開發中常見問題解決方案的外掛。

Controller 路由定義

export class HomeController {
  @route(`/api/xxx`, { name: `獲取XXX資料` })
  async getXXX(size: number, page: number) {
    return `homeIndex`;
  }
}

可以看到,使用 decorator 的形式來宣告 Controller 非常直觀,而且方便擴充套件,新增/修改 Controller 規則直接修改 decorator 的型別定義就好。這種形式也是 Java/C# 的常規操作。

這裡的改進除了使用 decorator 替代了 router.js 來進行 Controller 宣告以外,還新增了出入參支援,省去了需要手動讀寫 ctx 的過程,非常直觀的宣告 Controller 函式的引數需求,以及返回資料型別。

基於 decorator 的寫法與之前最大的區別是,在 Controller 這個橫切面,之前只有 loader 可以掌控,而現在可以在 decorator 中加以集中控制,再結合 TypeScript 的元資訊,可以做出很多擴充套件,比如:

引數格式化

在 eggjs 中,因為沒有型別資訊的原因,從 params 和 query 中獲取的資訊都會是字串型別,都需要在 Controller 中手動轉換。而改造之後的寫法,引數直接暴露在函式入參裡,我們就可以直接拿寫在入參的型別定義作為格式化的依據,根據型別嘗試轉換,保證引數型別正確,可以初步防止型別不符的引數進入到 Controller,省去手動判斷、轉換的邏輯。

引數校驗

引數格式化只能保證引數的型別一致性,而我們的需求不止這些,比如必選引數為空時需要攔截,有時引數是複雜物件為了防止惡意構造資料,需要對資料格式做深度檢測,所以這裡引入了引數校驗庫,parameter,通過它來解決複雜的校驗問題。

export class HomeController {
  @route(`/api/xxx`, { name: `獲取XXX資料`, validateMetaInfo: [{
    name: `data`,
    rule: {
      type: `object`,
      str: { type: `string`, max: 20 },
      count: { type: `number`, max: 10, required: false },
    },
   }] })
  async getXXX(data: { str: string, count?: number }>) {
    return data.str;
  }
}

這裡有個問題,在型別是複雜型別時,TypeScript 預設生成的後設資料裡,獲取不到型別的具體欄位屬性資訊,而且前端直接引入複用後端的型別定義也比較麻煩。所以,想要在定義型別的同時,複用型別的定義,只能在編譯時做工作,TypeScript 也開放出了編譯時外掛API,在不用編譯時外掛的情況下,就需要單獨寫一份規則的資料。

有外掛後:

export class HomeController {
  @route(`/api/xxx`, { name: `獲取XXX資料` })
  async getXXX(data: BaseValidateRule<{
    type: `object`,
    rule: {
      str: { type: `string`, max: 20 },
      count: { type: `number`, max: 10, required: false },
    },
  }>) {
    return data.str;
  }
}

路由級中介軟體

函式型別跟 egg 定義稍有不同:

(app: Application, typeInfo: RouteType) => (ctx: any, next: any) => any

egg 已經定義了中介軟體,為什麼在路由上還定義一個?在路由定義的中介軟體跟全域性的中介軟體區別在於範圍,全域性中介軟體更適合大規模的統一處理,用來統一處理特定業務功能介面就大材小用了,還需要設定過濾邏輯,甚至需要在 config 中設定黑白名單。而路由級中介軟體適合只有部分介面需要的統一處理,配合從 @route 上收集的型別資訊處理更佳。

API文件 & 前端SDK

既然已經收集到了那麼多後設資料,根據這些資料生成API文件就很簡單了無非就是前端的展示,也可以把資料轉換對接其他的API文件平臺。

更近一步,直接生成前端呼叫SDK?當然沒問題。本外掛支援通過模板生成,如果沒有找到模板,會在SDK生成目錄生成預設模板。

// Controller
export class MetaController {

  @route({ url: `/meta/index`, name: `首頁` })
  async index(id: string, n: number, e: `enumA` | `enumB`, d: Date) {
    return `metaIndex`;
  }

}

// 生成程式碼
export class MetaService extends Base {

  /** 首頁  */
  async index(id: string, n: number, e: string, d: Date) {
    const __data = { id, n, e, d };
    return await this.request({
      method: `get`,
      url: `/meta/index`,
      data: __data,
    });
  }

}

export const metaService = new MetaService();
export default new MetaService();

開發時

在配置中開啟即可,根據需要自定義其他配置。當 Controller 中檔案修改時,會同時重新生成對應的前端SDK檔案。

構建打包時

在 前端打包流程前 可以使用 egg-controller gensdk 命令生成前端sdk,需要注意,如果為 TypeScript 專案,需要先將 TypeScript 編譯,然後執行生成命令。

Controller 作為請求的起點,這只是個開始。

egg 框架

egg-controller 詳細文件