用 Express & Sequelize 打造 MVC 餐廳網站(上)


Posted by Nicolakacha on 2020-10-25

嗨嗨,這篇文章主要是程式導師計畫 week 17~18 的實作心得 XD 在本文中,會用 後端框架 ExpressORM 工具 Sequelize 來打造 MVC 架構的餐廳網站(對,餐廳網站又要進化了),但 Express 和 Sequelize 的基本用法在官方文件中都已經有很完整的說明了,所以不會著墨太多,而本文的主要內容會比較傾向於關注架構 MVC 的起手式、實作時的思路方向,以及記錄我在時做時遇到的,覺得值得紀錄的困難點。那我們就開始吧!

等...等等,先來簡單一下介紹 MVC 模式好了。

MVC 架構簡介

MVC 是一種應用程式架構,將程式碼拆分成 Model, View 以及 Controller 三個部份,並透過路由的設計,建立起整個應用程式的邏輯:

  • Model 是處理資料的部份,例如在 MySQL 資料庫裡建立 tables,以及所有讀取、寫入資料庫的程式,都可以歸類在 Model
  • View 則是處理畫面的部份,包括了我們所有能看到的內容,也就是我們要 Response 給 Client 端的內容,像是網站上所有的 HTML。
  • Controller 則是整個應用程式的邏輯,並銜接 Model 與 View。當在不同路由收到不同的 HTTP Request 時,呼叫 Controller 的執行相應的 Method,例如渲染畫面、呼叫 Models 向資料庫拿取資料,再發送 Response 等,都是屬於 Controller 要做的事。

安裝完 Express 之後,我們就正式開...開始吧!這次的餐廳網站的規格主要會有餐廳網站和管理後台兩個的部份:

  • 登入、註冊頁面
  • 餐廳網站:餐廳首頁、我要點餐、抽個大獎、常見問題、購物車
  • 管理後台:後台首頁、菜單管理、新增菜單、獎項管理、新增獎項、問題管理、新增問題

功能面會有這些:

  • 身為管理員,我能夠登入和登出
  • 身為管理員,我能夠新增、刪除、修改常見問題
  • 身為管理員,我能夠新增、刪除、修改 menu 菜單
  • 身為管理員,我能夠新增、刪除、修改抽獎獎項
  • 身為使用者,我能夠將商品加入跨頁面的購物車中
  • 身為使用者,我能夠玩抽獎遊戲

除了餐廳首頁之外,有涉及常見問題、獎項、菜單的頁面,都需要跟資料庫溝通拿資料,但此時後端的功能都還沒做,如果沒有資料可能很難去調整版面設計,所以決定先把以上頁面都先當成靜態頁面,在上面做一些假的資料。這樣就可以先處理版面設計。

初始化工作

一週後所有頁面的版都切好了,可以先擺在旁邊,我們先來做一些 Express 框架及 MVC 架構的初始化工作:

  • 建立專案資料夾,裡面建好 views, controllers, models 三個資料夾
  • 安裝 express,並建立應用程式的入口點檔案 app.js
// 引入 express
const express = require('express'); 

// 建立應用程式
const app = express();

// 設定伺服器使用哪一個 port
const port = 5556;

// 設定 template engine,之後會再說明
app.set('view engine', 'ejs');

// 應用程式監聽哪一個 port
app.listen(port, () => {
  console.log(`express restaurant app listening on port ${port}`);
});

Template Engine

上面 app.js 有設定引入 ejs 這個 template engine,所以還需要透過 npm 安裝 ejs,有了 ejs,之後我們就能在開發 PHP 網站那樣把程式和 HTML 寫在一起。

接著就把切好的 HTML 們都放入 views 資料夾變成 ejs 檔吧,當然,像開發 PHP 一樣,重複的區塊像是 head 和 footer,也可以放在另外的 template 資料夾再 include 進來,餐廳網站首頁的 ejs 檔案會像這樣,因為還沒用上 JavaScript 來傳資料進去,現在看起來跟 HTML 有 87% 像,只是多了一些 include:

<!DOCTYPE html>
<html lang="zh-TW">
  <head>
    <%- include ('templates/head') %>
    <script type="text/javascript" src="/js/index.js"></script>
  </head>

  <body>
    <%- include ('templates/nav') %>

    <section class="banner index">
      <h1>咬一口廚房</h1>
    </section>
    ... 略

ejs 語法

  • <%-   %> 會印出來且被解析,一般適用於引入 html,因為我們需要 ejs 引擎識別 html 並解析,可以想成是 PHP 的 <?PHP echo   ?>
  • <%=   %> 會印出來且不會被解析語法,可以用在輸出資料上,因為不會被解析成語法,等於自帶 XSS 防禦
  • <%   %> 不會被印出來,會被當成 JavaScript 執行,就像是 PHP 中的 <?PHP   ?>

詳細使用方法請看 ejs 官方文件

其他頁面也照樣完成就可以惹,接著要來處理每個頁面的 render 分別要交給哪些 controller。

設計 controller 及 route

李組長眉頭一皺,開出了這五個 controller 檔案分別處理不同頁面的 render:

  • user.js:登入、註冊
  • prize.js:抽獎、管理抽獎、新增抽獎
  • question.js:常見問題、管理問題、新增問題
  • menu.js:我要點餐、管理菜單、新增菜單
  • order.js:購物車

這些 controllers 就是我們要放在 /controller 的資料夾中的,並把它們像這樣都引入入口點 app.js:

// app.js
const userController = require('/controllers/user');
... 略

接著在 app.js,把每個頁面的路由,也就是網址路徑都設計好,app.get() 中的第一個參數就是路由、第二個參數則是收到 HTTP get Request 時,要執行哪個 controllers 裡面的哪個 method,因為我們要執行的是頁面渲染,之後所有的操作都會是透過這樣的模式:

  • 在哪個路由接收到什麼樣的 HTTP Request 時
  • 要交給哪個 controller
  • 執行該 controller 的哪個 method。

但是因為 controller 裡面的 method 都還沒做,我們可以先為這些渲染頁面的 method 取個名字,之後再去每個 controller 裡面負責 render 的 method 實際做出來。以下面的例子來說直接以 register 來代表渲染註冊頁面,.login 代表渲染登入頁面的那個 method,依此類推:

// index.js
app.get('/register', userController.register)
app.get('/login', userController.login)
app.get('/logout', userController.logout);

app.get('/', userController.home);
app.get('/menu', menuController.menu);
app.get('/question', questionController.question);
app.get('/prize', prizeController.prize);
...略

做好 controller 內的 methods

接下來我們就只要把剛才命名好的 method 們在每個 controller 裡面做出來就可以了,以 userController 為例,要負責渲染的畫面有餐廳首頁、登入、註冊、管理後台:

// controllers/user.js
const userController = {
  login: (req, res) => {
    res.render('login');
  },

  register: (req, res) => {
    res.render('register');
  },

  home: (req, res) => {
    res.render('index');
  },

  manage: (req, res) => {
    res.render('manage');
  },
};

// 要記得 export 出去
module.exports = userController;

建立 middleware

Express 的核心就是 routing 和 middleware,routing 就是路由的部份,藉由定義不同路由來執行接收到 request 所觸發的一系列 middleware。而 middleware function 可以在第一個參數拿到 request, 第二個參數發送 response,並透過第三個參數 next 把控制權移轉給下一個 middleware function。

如果不設定路由,那麼 middleware 就會在 app 每次收到任何 Request 的時候執行:

app.use(function (req, res, next) {
  console.log('Time:', Date.now())
  next()
})
login: (req, res) => {
res.render('login');
},

上面的 render 的 method,意思就是接收到 request 時,response 要 render login 這個頁面,而 login 就是我們放在 views 裡面的 ejs 檔案的檔名。

其他 controllers 也比照辦理,完成了以後,我們的靜態網站就完成啦!等等等一下,突然發現 CSS 都沒有被載入進來耶 QAQ,原本常見問題頁面需要用到的 JavaScript 也壞掉了啊啊啊!

載入 CSS 和前端 JavaScript

這是因為,在 Express 框架中,這些靜態檔案要使用 static 這個 middleware 來載入,所以我們在 app.js 中需要使用:

app.use(express.static(`${__dirname}/public`));

把 CSS、前端 JavaScript 和圖片等靜態檔案都放在根目錄下的 public 資料夾,這麼一來,views 裡面的 ejs 就都可以正確載入惹!

routing - middleware

可以發現我們都是遵循一樣的模式,在 app.js 建立不同的 routing - middleware,來完成我們需要做的事:

  1. 在哪個路由接收到什麼樣的 HTTP Request 時,
  2. 要交給哪個 controller,
  3. 執行該 controller 的哪個 method。

以新增常見問題的功能為例:

  1. 在新增問題頁面接收到 POST Request 時,
  2. 要交給 prizeContoller
  3. 執行 handleAdd 這個 method 來把資料新增給資料庫

實作功能時,也會些自己建立的或第三方的 middleware 來幫忙,這些 middleware 可以加入我們上面的 routing - middleware 流程,在剛才的範例中,實際上新增資料時,還需要檢查管理員的登入狀態,所以會加入驗證身份的 middleware,並在確認有登入的時候把控制權交給 questionController.handleAdd,像是這樣:

function checkLogin(req, res, next) {
  //...檢查是否有登入
  next();
}
app.post('/manage/question/add', checkLogin, questionController.handleAdd);

啟動 app

在 command line 執行 node app.js 就可以啟動伺服器,並在所監聽的 port 上面訪問網頁:

也可以在 package.json 中的 scripts 做設定,例如以下設定可以用 npm run localStart 的指令啟動伺服器:

"scripts": {
    "localStart": "node app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

總結

到這裡,我們的靜態頁面版的餐廳網站,路由和渲染都完成了,接著只要依循著相同的模式,建立 routing 和 middleware,就能把需要的功能都做出來囉,下篇文章會來實作完整的功能,並且使用 Sequelize 連接資料庫,是不是很興奮呢? 我們下篇文章見~


#MVC #javascript #Express #Backend







Related Posts

[Golang] Tags

[Golang] Tags

Unreal Webcam Fun

Unreal Webcam Fun

『Android』ADB Debug by wifi 用Wifi連線來Debug

『Android』ADB Debug by wifi 用Wifi連線來Debug


Comments