iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 16
0
Software Development

今晚我想來點 Express 佐 MVC 分層架構系列 第 16

[今晚我想來點 Express 佐 MVC 分層架構] DAY 16 - Controller 與 Exception

前一篇的小結有提到現在的回傳與錯誤處理機制不夠自動化,每次都要輸入 res.status(status).json(obj) ,這樣不夠 懶惰 聰明。我們先從錯誤處理開始,那要如何做錯誤處理呢?就是寫 Exception

Exception

什麼是 Exception?當 例外情況發生所做的決策處理,換句話說就是錯誤處理,前面有提過在 Express 中要做全域錯誤處理就是寫中介軟體,所以 Exception 就是寫中介軟體。新增一個名為 exceptions 的資料夾,並建立 default.exception.ts

import { ErrorRequestHandler } from 'express';

export const DefaultException: ErrorRequestHandler = (err, req, res, next) => res.status(err.status).json(err);

可以看到上面有出現 err.status,一般來說 Error 物件是沒有 status 的,那是從哪裡來的呢?其實就是 ResponseObject ,我們在 Controller 將錯誤轉換成 ResponseObject,再把它傳給 Exception 做處理。

套用 Exception

前面有提過 Express 全域錯誤處理要在應用程式層進行,所以要去修改 App 的內容,我們設計一個 setException 的方法來設置 Exception:

public setException(handler: ErrorRequestHandler): void {
  this.app.use(handler);
}

index.ts 中啟用 DefaultException

import { App } from './app';
import { DefaultException } from './exceptions/default.exception';

const bootstrap = () => {
  const app = new App();
  app.setException(DefaultException);
  app.bootstrap();
};

bootstrap();

用 Wrap 處理回傳資訊

回想一下前面的文章有提過可以運用 await/async 來簡化錯誤處理的方式,其實它同樣可以用來簡化回傳資訊,只要讓 Controller 回傳的物件都設為 ResponseObject ,就可以從裡面取得回傳所需的資訊,再透過 res.status(status).json(obj) 送出。修改一下 RouteBase

import { Router, Request, Response, NextFunction } from 'express';
import { ControllerBase } from './controller.base';

import { HttpStatus } from '../types/response.type';

import { ResponseObject } from '../common/response/response.object';

export abstract class RouteBase {

  public router = Router();
  protected controller!: ControllerBase;

  constructor() {
    this.initial();
  }

  protected initial(): void {
    this.registerRoute();
  }

  protected abstract registerRoute(): void;

  protected responseHandler(method: (req: Request, res: Response, next: NextFunction) => Promise<ResponseObject>, controller = this.controller) {
    return (req: Request, res: Response, next: NextFunction) => {
      method.call(this.controller, req, res, next)
        .then(obj => res.status(obj.status).json(obj))
        .catch((err: Error) => next(controller.formatResponse(err.message, (err as any).status || HttpStatus.INTERNAL_ERROR)));
    };
  }

}

修改 TodoController 的回傳格式:

import { ControllerBase } from '../../../bases/controller.base';
import { ResponseObject } from '../../../common/response/response.object';
import { HttpStatus } from '../../../types/response.type';

export class TodoController extends ControllerBase {

  public async getTodos(): Promise<ResponseObject> {
    return this.formatResponse([], HttpStatus.OK);
  }

}

若有需要使用 RequestResponseNextFunction 的話,只要補上參數即可:

public async getTodos(req: Request): Promise<ResponseObject> {
  console.log(req.headers);
  return this.formatResponse([], HttpStatus.OK);
}

套用 ResponseHandler

將每個 Controller 的方法都套用 responseHandler,下方為 todo.routing.ts 的程式碼:

import { RouteBase } from '../../../bases/route.base';
import { TodoController } from './todo.controller';

export class TodoRoute extends RouteBase {

  protected controller!: TodoController;

  constructor() {
    super();
  }

  protected initial(): void {
    this.controller = new TodoController();
    super.initial();
  }

  protected registerRoute(): void {
    this.router.get(
      '/',
      this.responseHandler(this.controller.getTodos)
    );
  }

}

來確認一下資料夾結構:

├── src
|   ├── index.ts
|   ├── app.ts
|   ├── app.routing.ts
|   ├── bases
|   |   ├── route.base.ts
|   |   └── controller.base.ts
|   ├── common
|   |   └── response
|   |       └── response.object.ts
|   ├── exceptions                       //本篇新增
|   |   └── default.exception.ts         //本篇新增
|   ├── main
|   |   └── api
|   |       ├── api.routing.ts
|   |       └── todo
|   |           ├── todo.controller.ts
|   |           └── todo.routing.ts
|   ├── types
|   |   └── response.type.ts
|   ├── environments
|   |   ├── development.env
|   |   └── production.env
|   └── validators
|       ├── index.ts
|       └── email.validator.ts
├── package.json
└── tsconfig.json

規範 Controller

為了方便維護系統,我在這邊與大家分享我自己的規範:

處理 Request / Response

RouteModule 把邏輯的部分分到 Controller 中,減少了 RouteModule 處理邏輯的程式碼,此時的 Controller 就要負責從 Model 取得相關資源並回覆給客戶端。

盡量減少邏輯

不是把邏輯分給 Controller 了嗎?為什麼又要盡可能減少邏輯呢?因為過多的邏輯會使得 Controller 過於笨重, 維護上會較為困難 ,且 重用性較低 ,應該要再將邏輯切細一點分配到其他地方,這部分先埋個伏筆,後面會再跟大家說,先有個概念就好!

小結

Controller 與 RouteModule 有著互補關係,基本上他們都屬於 Controller 的範疇,只是我認為應該要把路由策略與處理邏輯分開,這樣會比較好維護。既然說到了要從 Model 取資源回覆給客戶端,那就必須先跟各位交代一下 Model 的部分,下一篇就決定是 Model 了!


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 15 - Controller
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 17 - Model
系列文
今晚我想來點 Express 佐 MVC 分層架構30

尚未有邦友留言

立即登入留言