Controller 最主要的任務就是負責收發請求,通常在回傳資訊的時候,會用統一的格式進行回傳,統一格式的部分可以透過 Response Object 來處理、回傳的資料格式為 DTO(Data Transfer Object) 所管、回傳資訊則透過 Controller 進行。
什麼是 DTO 呢? 資料用於傳輸的物件 就是所謂的 DTO,其用途廣泛,通常用於過濾、格式化資料,使資料變資訊的好幫手,也因為只負責存放要傳遞的資訊,故它 只有唯讀屬性 ,沒有任何方法。常見的場景:從資料庫拿來的資料不見得每一個欄位都要回傳給用戶,這時候就要把重要的留下、不重要的過濾掉,成為一個全新的物件。
什麼是 Response Object 呢?就是 傳輸給用戶的物件 ,與 DTO 不同的是它可以 包涵一個或多個的 DTO ,以及 status 等資訊。我們建立一個 common
資料夾,並在裡面建立一個包含 response.object.ts
的 response
資料夾,下方為 response.object.ts
的程式碼,status
為存放 HttpCode 的欄位、 data
為正常回傳資訊時帶入的欄位、 message
為錯誤時資訊帶入的欄位:
正常回傳資訊大致上可以用 HttpCode 400 為界線,400 以上為錯誤
import { HttpStatus } from '../../types/response.type';
export class ResponseObject {
public readonly status: HttpStatus = HttpStatus.INTERNAL_ERROR;
public readonly message: string = '';
public readonly data: any = null;
constructor(options: { status?: HttpStatus, message?: string, data?: any } = {}) {
this.status = options.status || this.status;
this.message = options.message || this.message;
this.data = options.data || this.data;
}
}
可以看到有一個來自 types
資料夾下的 response.type.ts
匯入,那是我用來放 HttpCode 的地方:
export const enum HttpStatus {
OK = 200,
CREATED = 201,
NO_CONTENT = 204,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
CONFLICT = 409,
UNPROCESSABLE = 422,
INTERNAL_ERROR = 500
};
Controller 最主要的目的是收發請求,故會使用到 ResponseObject
,我們可以將產生 ResponseObject
的動作抽離出來放進 ControllerBase
,所以在 bases
資料夾下新增一個 controller.base.ts
:
import { HttpStatus } from '../types/response.type';
import { ResponseObject } from '../common/response/response.object';
export abstract class ControllerBase {
public formatResponse(data: any, status = HttpStatus.INTERNAL_ERROR): ResponseObject {
const options: any = { status };
status >= 400
? options.message = data
: options.data = data;
const responseObject = new ResponseObject(options);
return responseObject;
}
}
產生 ResponseObject
只需要帶入要回傳的資訊以及 HttpCode,會根據所帶入的 HttpCode 來決定把資訊在 message
還是 data
裡。
完成 ControllerBase
以後,就可以來實作 Todo 的 Controller 了,我們先整理一下現在的資料夾結構,以防有遺漏掉的部分:
├── src
| ├── index.ts
| ├── app.ts
| ├── app.routing.ts
| ├── bases
| | ├── route.base.ts
| | └── controller.base.ts // 本篇新增
| ├── common // 本篇新增
| | └── response // 本篇新增
| | └── response.object.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
確認沒問題後,就來寫 todo.controller.ts
:
import { Request, Response, NextFunction } from 'express';
import { ControllerBase } from '../../../bases/controller.base';
import { HttpStatus } from '../../../types/response.type';
export class TodoController extends ControllerBase {
public async getTodos(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
const obj = this.formatResponse([], HttpStatus.OK);
res.status(obj.status).json(obj);
}
}
在 todo.routing.ts
中產生實例,並設置在 /api/todos
可以取得資訊:
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('/', (req ,res, next) => this.controller.getTodos(req, res, next));
}
}
上一篇在
api.routing.ts
是設置/todo
,這邊要去更改成/todos
在瀏覽器中輸入 http://localhost:3000/api/todos 即可看到結果:
今天認識到 DTO 與 ResponseObject 的概念,以及完成 Controller 的配置,但目前的結構需要在每個 Controller 的 method 裡面做錯誤處理以及回傳資訊給客戶端,其實可以用更「自動化」一點的方式來處理,下一篇將會告訴大家要如何自動處理這些事情!
Hello HAO
我在跟著練習時,在寫到todo.routing.ts時注意到在使用this.controller.getTodos時特別使用了arrow function。
我原本覺得看起來有點累贅,後來就將他改成
export class TodoRoute extends RouteBase {
// ...
protected registerRoute(): void {
this.router.get('/', this.controller.getTodos);
}
}
結果就發生了一個錯誤
這時候回頭看了一下TodoController
才注意到這邊使用了this,知道了arrow function是用來將this固定在方法在使用this時的指向。
我在想或許有一個更好的方式是取代this,改使用super?
export class TodoController extends ControllerBase {
public async getTodos(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
const obj = super.formatResponse(req.query, HttpStatus.OK)
res.status(obj.status).json(obj);
}
}
你好,也可以將 TodoController
的 method 改成 arrow function,這樣就會綁定 this
囉。如下:
export class TodoController extends ControllerBase {
public getTodos = async(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> => {
const obj = this.formatResponse(req.query, HttpStatus.OK)
res.status(obj.status).json(obj);
}
}