當我開始完善各個功能時,就發現回傳 view 所需要的函式不只有 render,在某些時候還是要使用 redirect,而當這樣會有兩種格式要回傳時,我就會建立 Response Object 來制定回傳的格式,並且在 route.base 中就要多一個專門處理 redirect 的函式。

當這些都完成後,就可以開始實作網頁的功能了,在這篇文章裡我會實作 MenuItem 的 CRUD。

Github:et860525/restaurant-management

Response Object

首先,Response object 會將 Contoller 所回傳的資料做成統一的格式,並能讓 Route 使用。以下將兩個 Response object 放到 src/commom/response/response.object.ts

export class renderResponseObject {
  public readonly view: string = 'index';
  public readonly data: any = null;

  constructor(options: { view: string; data?: any }) {
    this.view = options.view || this.view;
    this.data = options.data || this.data;
  }
}

export class redirectResponseObject {
  public readonly status: number = 302;
  public readonly url: string = '/';

  constructor(options: { url: string; status?: number }) {
    this.url = options.url || this.url;
    this.status = options.status || this.status;
  }
}

根據 renderredirect 會使用到的參數製作成 obejct,完成後再到 controller.base 新增相對應的函式來建立 Response object 並回傳:

import {
  renderResponseObject,
  redirectResponseObject,
} from '../common/response/response.object';

export abstract class ControllerBase {
  public formatResponse(view: string, data?: any) {
    const responseObject = new renderResponseObject({ view, data });

    return responseObject;
  }

  public formatRedirectResponse(url: string, status?: number) {
    const responseObject = new redirectResponseObject({ url, status });

    return responseObject;
  }
}

由 Route 會獲得從 Controll 回傳來的 Response object,所以要更改 route.base 的程式碼:

import { renderResponseObject, redirectResponseObject } from '../common/response/response.object';

export abstract class RouteBase {

  // 略...
  protected responseHandler(
    method: (
      req: Request,
      res: Response,
      next: NextFunction
    ) => Promise<renderResponseObject>
  ) {
    return (req: Request, res: Response, next: NextFunction) => {
      method
        .call(this.controller, req, res, next)
        .then((obj) => res.render(obj.view, obj.data))
        .catch((err) => next(err));
    };
  }

  protected responseRedirectHandler(
    method: (
      req: Request,
      res: Response,
      next: NextFunction
    ) => Promise<redirectResponseObject>
  ) {
    return (req: Request, res: Response, next: NextFunction) => {
      method
        .call(this.controller, req, res, next)
        .then((obj) => res.redirect(obj.status, obj.url))
        .catch((err) => next(err));
    };
  }
}

接下來就要開始完善 MenuItem 的各種功能。

以下是讓使用者可以對菜單進行 CRUD 的動作,首先先新增路由,到 main/menu/menuItem.routing.ts

import express from 'express';
import { RouteBase } from '../../base/route.base';
import { MenuItemController } from './menuItem.controller';

export class MenuItemRoute extends RouteBase {
  protected controller!: MenuItemController;

  constructor() {
    super();
  }

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

  protected registerRoute(): void {
    this.router.get(
      '/menuItems',
      this.responseHandler(this.controller.get_many)
    );
    this.router
      .route('/menuItems/create')
      .get(this.responseHandler(this.controller.form))
      .post(
        express.json(),
        this.responseRedirectHandler(this.controller.create)
      );
    this.router.get(
      '/menuItems/:id',
      this.responseHandler(this.controller.get)
    );
    this.router
      .route('/menuItems/:id/delete')
      .get(this.responseHandler(this.controller.delete_get))
      .post(this.responseRedirectHandler(this.controller.delete));
    this.router
      .route('/menuItems/:id/update')
      .get(this.responseHandler(this.controller.update_get))
      .post(
        express.json(),
        this.responseRedirectHandler(this.controller.update)
      );
  }
}
  • /menuItems:MenuItem 的主頁面
  • '/menuItems/:id':讀取指定 ID 的 MenuItem
  • '/menuItems/create':新增
  • '/menuItems/update':更新
  • '/menuItems/delete':刪除

接著開始實作各個路由的功能,到 main/menu/menuItem.controller.ts

前置

import { MenuItem } from '@prisma/client';
import { Request } from 'express';
import { ControllerBase } from '../../base/controller.base';
import { MenuItemService } from './menuItem.service';

export class MenuItemController extends ControllerBase {
  private readonly menuService = new MenuItemService();
  // 實作的其他功能
}
  • menuService:Controller 會把資料傳給 Service,並等待 Service 傳回從 Database 讀取來的資料

有些功能會在開始執行前,會先確定該筆資料是否存在於資料庫,如果沒有就會出現錯誤,所以掌握錯誤就很重要。建立一個檢查從資料庫回傳回來的資料是否為空的,如果是空的那就顯示錯誤的資訊:

 private checkMenuItem = async (id: string, menuItem: MenuItem | null) => {
    if (menuItem === null) {
      return this.formatResponse('error', {
        error: new Error(`MenuItem ${id} is not exist`),
      });
    } else {
      return menuItem;
    }
};

Create

public async form() {
    return this.formatResponse('menuItem_form');
}

public async create(req: Request) {
	const { name, description, price } = req.body;
	const menuItem = await this.menuService.create(name, description, price);
	
	return this.formatRedirectResponse(`/menuItems/${menuItem.id}`);
}

Read one

public async get(req: Request) {
    const { id } = req.params;
    const menuItem = await this.menuService.get(Number(id));

    this.checkMenuItem(id, menuItem);

    return this.formatResponse('menuItem_detail', { menuItem: menuItem });
  }

Read many

public async get_many(req: Request) {
    const skip = req.query.skip || 0;
    const take = req.query.take || 10;

    const menuItems = await this.menuService.get_many(
      Number(skip),
      Number(take)
    );

    return this.formatResponse('menuItem', {
      menuItems: menuItems,
    });
}

Update

public async update_get(req: Request) {
    const { id } = req.params;
    const menuItem = await this.menuService.get(Number(id));
    this.checkMenuItem(id, menuItem);
    
    return this.formatResponse('menuItem_form', { data: menuItem });
}

public async update(req: Request) {
	const { id } = req.params;
	const { name, description, price } = req.body;
	
	const menuItem = await this.menuService.update(Number(id), {
	  name: name,
	  description: description,
	  price: Number(price),
	});
	
	return this.formatRedirectResponse(`/menuItems/${menuItem.id}`);
}

Delete

  public async delete_get(req: Request) {
    const { id } = req.params;
    const deleteUrl = req.path.split('/')[1];
    
    const menuItem = await this.menuService.get(Number(id));
    this.checkMenuItem(id, menuItem);
    
    return this.formatResponse('delete', {
      deleteItem: menuItem,
      deleteUrl: deleteUrl,
    });
}

  public async delete(req: Request) {
    const { id } = req.params;
    await this.menuService.delete(Number(id));
    return this.formatRedirectResponse('/menuItems');
}

View 的部分就很簡單,只要把後端傳來的資料依據自己喜歡的方式放置即可,如需要參考可以到 et860525/restaurant-management/views

結語

整個專案到這裡就告一段落了,剩下的其他 table 的功能基本上建立的方式都大致相同。這樣用 OOP 方式分開各個元件有幾個好處:

  • 專責分類:根據功用分開才不會讓單一支程式負擔太大
  • 清楚標示:分類後就能知道哪一個元件做哪一件事情,就不會 controller 還要獲得資料庫資料
  • 測試方便:可以對單一個元件進行測試