iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 18
0
Software Development

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

[今晚我想來點 Express 佐 MVC 分層架構] DAY 18 - 整合 Express MVC

  • 分享至 

  • xImage
  •  

前面幾篇已經把 Model 、 View 、 Controller 都交代完畢了,現在可以將它們組裝起來,變成一個簡單的 TodoList API Server。這時候應該會有人感到困惑:什麼? View 去哪裡了?為什麼沒有看到?由於我們設計的是 API Server,所以 View 的部分就是 ResponseObject,但確實還沒有設計 DTO,那廢話不多說,直接進入 DTO!

TodoDTO

先思考一下,我們在設計 Model 的時候,有抽離一個部分叫做 createdAtupdatedAt,這個基本上會是回傳給客戶端的重要資訊之一,所以可以將這部分出離出來做成 Base,另外還有一個重要資訊就是資料的 id 欄位,也一同併入 Base 中。在 bases 資料夾下,新增 dto.base.ts

export class DTOBase {

  public readonly _id!: string;
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;

  constructor(dto: DTOBase) {
    this._id = dto._id;
    this.createdAt = dto.createdAt;
    this.updatedAt = dto.updatedAt;
  }

}

接著,新增一個 dtos 資料夾並建立 todo.dto.ts,其中傳入的參數為 TodoDocument,因為資料會從 Document 來:

import { DTOBase } from '../bases/dto.base';
import { TodoDocument } from '../models/todo.model';

export class TodoDTO extends DTOBase {

  public readonly content!: string;
  public readonly completed!: boolean;

  constructor(doc: TodoDocument) {
    super(doc);
    this.content = doc.content;
    this.completed = doc.completed;
  }

}

進行整合

我們說 Controller 就是 View 與 Model 之間的橋樑,所以我們要在 Controller 進行整合,修改 todo.controller.ts,設計 新增 Todo取得 Todos取得 Todo完成 Todo 以及 刪除 Todo 的功能:

新增 Todo

新增通常會使用 POST 方法,並會把相關資訊放在 body 中,所以要在 TodoController 中新增 addTodo這個方法,並從 Request 的 body 獲得 Todo 的 content,透過 TodoModel 進行新增,最後就是把存入的資訊用 TodoDTO 包裝起來,再傳入 ResponseObject 中並回傳給客戶端:

public async addTodo(req: Request): Promise<ResponseObject> {
  const { content } = req.body;
  const todo = new TodoModel({ content, completed: false });
  const document = await todo.save();
  const dto = new TodoDTO(document);
  return this.formatResponse(dto, HttpStatus.CREATED);
}

修改 TodoRoute

protected registerRoute(): void {
  this.router.route('/')
    .post(
      express.json(),
      this.responseHandler(this.controller.addTodo)
    );
}

透過 Postman 看結果:
https://ithelp.ithome.com.tw/upload/images/20200901/20119338esLrYCUWqH.png

取得 Todos

取得通常會使用 GET 方法,並將查詢參數放在 QueryString 及 Parameters,我們在 TodoController 中新增 getTodos 這個方法,並從 QueryString 獲得 skiplimit,以判斷要跳過幾筆、取幾筆資料,這裡需要特別注意的是,如果沒有帶參數的話應該給予預設值以及 limit 的上限,所以要在 types 資料夾下,新增 request.type.ts,並使用 enum 來設置預設值:

export const enum DefaultQuery {
  SKIP = 0,
  LIMIT = 30,
  MAX_LIMIT = 100
};

TodoController 中新增 getTodos 方法:

public async getTodos(req: Request): Promise<ResponseObject> {
  const { limit, skip } = req.query;
  const truthLimit = Math.min(Number(limit), DefaultQuery.MAX_LIMIT) || DefaultQuery.LIMIT;
  const truthSkip = Number(skip) || DefaultQuery.SKIP;
  const todos = await TodoModel.find().skip(truthSkip).limit(truthLimit);
  const dtos = todos.map(todo => new TodoDTO(todo));
  return this.formatResponse(dtos, HttpStatus.OK);
}

修改 TodoRoute

protected registerRoute(): void {
  this.router.route('/')
    .get(
      this.responseHandler(this.controller.getTodos)
    )
    .post(
      express.json(),
      this.responseHandler(this.controller.addTodo)
    );
}

透過 Postman 看結果:
https://ithelp.ithome.com.tw/upload/images/20200901/20119338HQhBfhBSEB.png

完成 Todo

TodoList 都會設置一個功能:當該筆項目完成的時候會勾選起來以表示完成,我們要來實作這個功能,這種操作行為屬於更新,所以使用 PATCH 方法,從 Request 的 body 獲得 Todo 的 completed 狀態,並從 Parameters 取得該筆 Todo 的 id。在 TodoController 新增 completedTodo 這個方法:

public async completedTodo(req: Request): Promise<ResponseObject> {
  const { id } = req.params;
  const { completed } = req.body;
  const options: QueryFindOneAndUpdateOptions = {
    new: true,
    runValidators: true
  };
  const todo = await TodoModel.findByIdAndUpdate(id, { completed }, options);
  if ( !todo ) {
    return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
  }
  const dto = new TodoDTO(todo);
  return this.formatResponse(dto, HttpStatus.OK);
}

修改 TodoRoute

protected registerRoute(): void {
  this.router.route('/')
    .get(
      this.responseHandler(this.controller.getTodos)
    )
    .post(
      express.json(),
      this.responseHandler(this.controller.addTodo)
    );
  this.router.patch(
    '/:id/completed',
    express.json(),
    this.responseHandler(this.controller.completedTodo)
  );
}

透過 Postman 看結果:
https://ithelp.ithome.com.tw/upload/images/20200901/20119338KFdK3RgH73.png

刪除 Todo

刪除通常會使用 DELETE 方法,在 TodoController 新增 removeTodo 這個方法,並從 Parameters 取得 Todo 的 id 來進行刪除:

public async removeTodo(req: Request): Promise<ResponseObject> {
  const { id } = req.params;
  const todo = await TodoModel.findByIdAndRemove(id);
  if ( !todo ) {
    return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
  }
  return this.formatResponse(null, HttpStatus.NO_CONTENT);
}

修改 TodoRoute

protected registerRoute(): void {
  this.router.route('/')
    .get(
      this.responseHandler(this.controller.getTodos)
    )
    .post(
      express.json(),
      this.responseHandler(this.controller.addTodo)
    );
  this.router.route('/:id')
    .delete(
      this.responseHandler(this.controller.removeTodo)
    );
  this.router.patch(
    '/:id/completed',
    express.json(),
    this.responseHandler(this.controller.completedTodo)
  );
}

透過 Postman 看結果:
https://ithelp.ithome.com.tw/upload/images/20200901/20119338tNZ5O35N3h.png

查詢 Todo

與查詢 Todos 差不多,但沒有 skiplimit,以及需要從 Parameters 取得 Todo 的 id,在 TodoController 新增 getTodo 這個方法:

public async getTodo(req: Request): Promise<ResponseObject> {
  const { id } = req.params;
  const todo = await TodoModel.findById(id);
  if ( !todo ) {
    return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
  }
  const dto = new TodoDTO(todo);
  return this.formatResponse(dto, HttpStatus.OK);
}

修改 TodoRoute

protected registerRoute(): void {
  this.router.route('/')
    .get(
      this.responseHandler(this.controller.getTodos)
    )
    .post(
      express.json(),
      this.responseHandler(this.controller.addTodo)
    );
  this.router.route('/:id')
    .get(
      this.responseHandler(this.controller.getTodo)
    )
    .delete(
      this.responseHandler(this.controller.removeTodo)
    );
  this.router.patch(
    '/:id/completed',
    express.json(),
    this.responseHandler(this.controller.completedTodo)
  );
}

透過 Postman 看結果:
https://ithelp.ithome.com.tw/upload/images/20200901/20119338M6Qvn0smNd.png

最後,來確認一下資料夾結構:

├── src
|   ├── index.ts
|   ├── app.ts
|   ├── app.routing.ts
|   ├── bases
|   |   ├── controller.base.ts
|   |   ├── route.base.ts
|   |   └── dto.base.ts               //本篇新增
|   ├── + common/resonse
|   ├── + exceptions
|   ├── main
|   |   └── api
|   |       ├── todo.controller.ts    //本篇修改
|   |       └── todo.routing.ts       //本篇修改
|   ├── + models
|   ├── dtos                          //本篇新增
|   |   └── todo.dto.ts               //本篇新增
|   ├── types
|   |   ├── model.type.ts
|   |   ├── response.type.ts
|   |   └── request.type.ts           //本篇新增
|   ├── + environments
|   ├── + database
|   └── + validators
├── package.json
└── tsconfig.json

小結

今天終於完成了基本的 MVC 架構了,不曉得大家有沒有注意到這個架構還是有點美中不足,也就是前面我提到的 Controller 過於笨重 的問題,現在的時代我們通常會把 MVC 做點變化,主要就是希望分散每個單元的負擔,這樣比較易於擴充與修改,下一篇將會介紹把 Controller 邏輯抽離的設計模式 - Service Layer Pattern


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 17 - Model
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 19 - Service Layer Pattern
系列文
今晚我想來點 Express 佐 MVC 分層架構30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言