iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 15
0
Software Development

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

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

概念

Controller 最主要的任務就是負責收發請求,通常在回傳資訊的時候,會用統一的格式進行回傳,統一格式的部分可以透過 Response Object 來處理、回傳的資料格式為 DTO(Data Transfer Object) 所管、回傳資訊則透過 Controller 進行。

DTO

什麼是 DTO 呢? 資料用於傳輸的物件 就是所謂的 DTO,其用途廣泛,通常用於過濾、格式化資料,使資料變資訊的好幫手,也因為只負責存放要傳遞的資訊,故它 只有唯讀屬性 ,沒有任何方法。常見的場景:從資料庫拿來的資料不見得每一個欄位都要回傳給用戶,這時候就要把重要的留下、不重要的過濾掉,成為一個全新的物件。
https://ithelp.ithome.com.tw/upload/images/20200826/20119338jGfyW3c3fG.png

Response Object

什麼是 Response Object 呢?就是 傳輸給用戶的物件 ,與 DTO 不同的是它可以 包涵一個或多個的 DTO ,以及 status 等資訊。我們建立一個 common 資料夾,並在裡面建立一個包含 response.object.tsresponse 資料夾,下方為 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
};

ControllerBase

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 裡。

實作 TodoController

完成 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 即可看到結果:
https://ithelp.ithome.com.tw/upload/images/20200828/201193380Cf4iqakUB.png

小結

今天認識到 DTO 與 ResponseObject 的概念,以及完成 Controller 的配置,但目前的結構需要在每個 Controller 的 method 裡面做錯誤處理以及回傳資訊給客戶端,其實可以用更「自動化」一點的方式來處理,下一篇將會告訴大家要如何自動處理這些事情!


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 14 - Route Module
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 16 - Controller 與 Exception
系列文
今晚我想來點 Express 佐 MVC 分層架構30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
tim80411
iT邦新手 5 級 ‧ 2023-03-07 20:31:48

Hello HAO
我在跟著練習時,在寫到todo.routing.ts時注意到在使用this.controller.getTodos時特別使用了arrow function。

我原本覺得看起來有點累贅,後來就將他改成

export class TodoRoute extends RouteBase {
  // ...

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

結果就發生了一個錯誤
圖 1

這時候回頭看了一下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);
  }
}
HAO iT邦研究生 3 級 ‧ 2023-03-19 11:37:34 檢舉

你好,也可以將 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);
  }
}

我要留言

立即登入留言