嗨嗨,這篇文章主要是程式導師計畫 week 17~18 的實作心得 XD 在本文中,會用 後端框架 Express 和 ORM 工具 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,來完成我們需要做的事:
- 在哪個路由接收到什麼樣的 HTTP Request 時,
- 要交給哪個 controller,
- 執行該 controller 的哪個 method。
以新增常見問題的功能為例:
- 在新增問題頁面接收到 POST Request 時,
- 要交給 prizeContoller
- 執行 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 連接資料庫,是不是很興奮呢? 我們下篇文章見~