前一篇的小結有提到現在的回傳與錯誤處理機制不夠自動化,每次都要輸入 res.status(status).json(obj)
,這樣不夠 懶惰 聰明。我們先從錯誤處理開始,那要如何做錯誤處理呢?就是寫 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 做處理。
前面有提過 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();
回想一下前面的文章有提過可以運用 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);
}
}
若有需要使用 Request
、 Response
或 NextFunction
的話,只要補上參數即可:
public async getTodos(req: Request): Promise<ResponseObject> {
console.log(req.headers);
return this.formatResponse([], HttpStatus.OK);
}
將每個 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
為了方便維護系統,我在這邊與大家分享我自己的規範:
RouteModule 把邏輯的部分分到 Controller 中,減少了 RouteModule 處理邏輯的程式碼,此時的 Controller 就要負責從 Model 取得相關資源並回覆給客戶端。
不是把邏輯分給 Controller 了嗎?為什麼又要盡可能減少邏輯呢?因為過多的邏輯會使得 Controller 過於笨重, 維護上會較為困難 ,且 重用性較低 ,應該要再將邏輯切細一點分配到其他地方,這部分先埋個伏筆,後面會再跟大家說,先有個概念就好!
Controller 與 RouteModule 有著互補關係,基本上他們都屬於 Controller 的範疇,只是我認為應該要把路由策略與處理邏輯分開,這樣會比較好維護。既然說到了要從 Model 取資源回覆給客戶端,那就必須先跟各位交代一下 Model 的部分,下一篇就決定是 Model 了!