本篇為 [BE201] 後端中階:Express 與 Sequelize 這門課程的學習筆記。如有錯誤歡迎指正!
在之前的課程中,我們學到如何使用 Express 這套框架,以及在 Express 扮演重要角色的 Middleware:
- [week 17] 後端中階 - 使用 Node.js + Express 框架建立一個靜態網頁
- [week 17] 後端中階 - Express 中不可或缺的拼圖:淺談 Middleware
接下來要整合我們之前所學的東西,透過串聯資料庫系統,來實作一個簡單的會員註冊系統,並且具有留言版功能的網頁。
實戰練習:簡易會員註冊系統
在使用 Express 框架實作網頁時,大致上會依照下方流程進行:
- 思考產品全貌:會員註冊系統
- 規劃資料庫結構:users table
- 載入需要的模組,設定 app 路由部分
- 依照 MVC 架構撰寫程式碼:
- 設定 Controller:針對不同路由進行控制
- 設定 Model:如何處理資料
- 設定 View:如何呈現畫面
接下來我們也同樣會依照這個流程來進行實作。
Step1. 規劃資料庫
首先在資料庫新增一個 users Tabel:
可以把 username 設定為 UNIQUE(唯一),只要 username 有重複就會自動跳出錯誤訊息:
Step2. 實作 Model 部分
接著要來建立 userModel:
const db = require('../db');
const userModel = {
// 新增 user 功能
add: (user, cb) => {
db.query(
'INSERT INTO users(username, password, nickname) VALUES(?, ?, ?)',
[user.username, user.password, user.nickname],
(err, results) => {
if (err) return cb(err);
cb(null);
}
);
},
// 登入 -> 讀取 user 功能
get: (username, cb) => {
db.query(
'SELECT * FROM users WHERE username = ?', [username], (err, results) => {
if (err) return cb(err);
cb(null, results[0]);
});
}
}
module.exports = userModel;
Step3. 實作 Controller 部分
- 先建立好程式碼架構,再來填寫路由部分:
// 引入 userModel
const userModel = require('../models/todo')
const userController = {
get: (req, res) => {
}
}
// 輸出 userController
module.exports = userController
- 在 index.js 引入 userController,並新增 userController 處理 login 和 logout 的路由:
const userController = require('./controllers/user')
app.get('/login', userController.login)
app.post('/login', userController.handleLogin)
app.get('/logout', userController.logout)
- Controller 就會根據不同路由,建立相對應的 Method:
// 引入 userModel
const userModel = require('../models/user')
const userController = {
// 渲染登入畫面
login: (req, res) => {
res.render('login')
},
// 驗證登入狀態
handleLogin: (req, res) => {
if (req.body.password) === 'abc' {
req.session.isLogin = true
res.redirect('/')
} else {
req.flash('errorMessage', 'Please input the correct password.')
}
},
// 登出: 清除 session 並導回首頁
logout:: (req, res) => {
req.session.isLogin = false;
res.redirect('/');
}
}
// 輸出 userController
module.exports = userController
Step4. 實作 View 部分
- 在根目錄增加一個首頁 index 的路由:
// 建立首頁
app.get('/', (req, res) => {
res.render('index')
})
- 接著建立 index.ejs 檔案,實作主要頁面:
<h1>簡易會員系統</h1>
<a href="/register">註冊</a>
<a href="/login">登入</a>
<a href="/logout">登出</a>
到這裡可先運行程式看看有沒有問題:
- 加入前面範例中的驗證功能:
<h1>簡易會員系統</h1>
<% if(isLogin) { %>
<a href="/logout">登出</a>
<% } else { %>
<a href="/register">註冊</a>
<a href="/login">登入</a>
<% } %>
這樣在未登入狀態下,就只會看到註冊和登入,這樣就完成了首頁 index 的部分:
Step5. 實作註冊功能
- 先建立 register 路由:
app.get('/register', userController.register)
app.post('/register', userController.handleRegister)
- 根據不同路由建立相對應的 controller,這裡可以把有關 user 的 view 放在同一個資料夾,較方便管理:
// 渲染註冊頁面
register: (req, res) => {
// 把路徑設在 user 資料夾,較方便管理
res.render('user/register');
},
// 驗證註冊
handleRegister: (req, res) => {
},
- 新增 register 的 view:
<h1>註冊頁面</h1>
<h2><%= errorMessage %></h2>
<form method="POST" action="/register">
<div>username: <input type="text" name="username" /></div>
<div>nickname: <input type="text" name="nickname" /></div>
<div>password: <input type="password" name="password" /></div>
<input type="submit" />
</form>
記得每做到一個段落就執行程式看有沒有出錯,這樣我們就完成簡單的註冊頁面。這時按提交還不會有反應:
- 再來就是實作 handleRegister 的 Controller 部分:
// 驗證註冊
handleRegister: (req, res) => {
// 從 resquest body 拿取 user 資料
const {username, password, nickname} = req.body;
if (!username || !password || !nickname) {
// 這裡用 return 就可避免 if-else 寫法增加層數
return req.flash('errorMessage', '缺少必要欄位');
}
// 資料都沒問題的話,就可透過 userModel 寫入資料
// 傳入一個物件,若有錯誤會回傳 cb
userModel.add({
username,
nickname,
password
}, (err) => {
if (err) {
// 印出 err
return req.flash('errorMessage', err.toString());
}
}
// 註冊成功就保持登入狀態,並導回首頁
req.session.username = username;
res.redirect('/');
});
},
logout: (req, res) => {
// 登出就把 session 重置
req.session.username = null;
res.redirect('/');
}
- 但這樣其實就要把 session 改成存 username,有 username 就代表有登入狀態:
// 透過 locals 傳值: session 功能和 errorMessage
app.use((req, res, next) => {
// 有 username 代表有登入狀態
res.locals.username = req.session.username
res.locals.errorMessage = req.flash('errorMessage')
next()
})
- 首頁 index.ejs 的驗證也同樣修改成判斷是否有 username,因為使用
<%= %>
會輸出原始碼,相當於做 escpae:
<h1>簡易會員系統</h1>
<!-- 以是否有 username 來判斷登入狀況 -->
<% if(username) { %>
<!-- 使用 <%= %> 會輸出原始碼,相當於做 escpae -->
<div>Hello, <%= username + '!' %></div>
<a href="/logout">登出</a>
<% } else { %>
<a href="/register">註冊</a>
<a href="/login">登入</a>
<% } %>
Step6. 用 hash 雜湊處理明碼
- 接下來可安裝 node.bcrypt.js 套件,即可將明碼進行雜湊處理,避免儲存明碼造成安全性問題:
$ npm install bcrypt
- 使用方法
// 引入 bcrypt
const bcrypt = require('bcrypt');
// 加鹽,增加密碼的複雜程度
const saltRounds = 10;
- 雜湊函式
bcrypt.hash(myPlaintextPassword, saltRounds, function(err, hash) {
// Store hash in your password DB.
});
- 再來修改剛才的 handleRegister,利用 bcrypt 套件對密碼進行雜湊處理,再儲存 hash 過的密碼到資料庫:
// 驗證註冊
handleRegister: (req, res, next) => {
// 從 resquest body 拿取 user 資料
const {username, password, nickname} = req.body;
if (!username || !password || !nickname) {
// 這裡用 return 就可避免 if-else 寫法增加層數
req.flash('errorMessage', '缺少必要欄位');
return next();
}
// 利用 bcrypt 套件對密碼進行雜湊處理
bcrypt.hash(password, saltRounds, function (err, hash) {
// 若有 err 就直接顯示錯誤訊息
if (err) {
req.flash('errorMessage', err.toString());
return next();
}
// 資料都沒問題的話,就可透過 userModel 寫入資料
userModel.add({
username,
nickname,
password: hash
}, (err) => {
// 若有 err 就直接顯示錯誤訊息
if (err) {
req.flash('errorMessage', '已存在相同用戶名');
return next();
}
// 註冊成功就保持登入狀態,並導回首頁
req.session.username = username;
res.redirect('/');
});
});
},
執行程式並註冊一個帳號,確認資料是否有成功寫進資料庫,密碼也有先經過 hash 才儲存:
這樣就差不多完成了註冊功能:
Step7. 實作登入功能
- 再來要修改登入頁面 login.ejs,畫面和註冊頁面很類似:
<h1>登入頁面</h1>
<h2><%= errorMessage %></h2>
<form method="POST" action="/login">
<div>username: <input type="text" name="username" /></div>
<div>password: <input type="password" name="password" /></div>
<input type="submit" />
</form>
- 然後是 handleLogin,確認是否有填入帳密、確認資料是否正確,透過 bcrypt.compare() 驗證雜湊密碼:
// 渲染登入畫面
login: (req, res) => {
res.render('login')
},
// 驗證登入狀態
handleLogin: (req, res, next) => {
const { username, password } = req.body;
// 確認是否有填入資料
if (!username || !password) {
req.flash('errorMessage', '請輸入您的帳密!');
// 每當呼叫 next 時,就會將控制權給下一個中間介 redirectBack(導回上一頁)
return next();
}
// 輸入正確就從 userModel 找出 user 資料
userModel.get(username, (err, user) => {
if (err) {
req.flash('errorMessage', err.toString());
return next();
}
if (!user) {
req.flash('errorMessage', '使用者不存在');
return next();
}
// 驗證密碼是否正確,三個參數代表: 明碼, 雜湊密碼, 方法
bcrypt.compare(password, user.password, function (err, isSccess) {
// 若出現錯誤或比對不成功,就顯示錯誤訊息
if (err || !isSccess) {
req.flash('errorMessage', '密碼錯誤');
return next();
}
req.session.username = user.username;
res.redirect('/')
});
})
},
// 登出: 清除 session 並導回首頁
logout: (req, res) => {
req.session.username = null;
res.redirect('/');
}
- 在登入和註冊頁面,每當呼叫 next 時,就會將控制權給下一個中間介,可以自己寫一個中間介 redirectBack,用來在提交表單後自動導回上一頁:
// 自己寫的中間介,用來導回上一頁
function redirectBack(req, res, next) {
res.redirect('back')
}
app.get('/register', userController.register)
app.post('/register', userController.handleRegister, redirectBack)
app.get('/login', userController.login)
app.post('/login', userController.handleLogin, redirectBack)
app.get('/logout', userController.logout)
能夠根據未填欄位、或是資料輸入錯誤,顯示相對應的錯誤訊息:
執行程式確認註冊和登入功能都沒問題的話,這樣就完成了簡易的會員註冊系統!藉由 MVC 架構,就能夠很清楚的分工,簡化程式邏輯。
實戰練習:有新增功能的簡易留言板
再來要結合會員系統,實作一個有新增功能的簡易留言版。那麼就趕緊開始吧!
Step1. 規劃資料庫
在 app database 新增一個 comments table,以 username 欄位和 users table 進行關聯
Step2. 新增留言區塊
在首頁 index.ejs 介面新增留言表單,如果是登入狀態,就能看到留言區塊:
<h1>簡易會員系統</h1>
<!-- 以是否有 username 來判斷是否登入 -->
<% if(username) { %>
<div>Hello, <%= username + '!' %></div>
<a href="/logout">登出</a>
<form method="POST" action="/comments">
<textarea name="content" id="" cols="30" rows="10"></textarea>
<input type="submit" />
</form>
<% } else { %>
<a href="/register">註冊</a>
<a href="/login">登入</a>
<% } %>
畫面看起來會是這個樣子:
Step3. 新增留言功能
- 新增路由:
const commentController = require('./controllers/comment');
// 提交表單來新增 comment,同樣以 redirectBack 來導回上一頁
app.post('/comments', commentController.add, redirectBack)
- 建立 commentConrtoller 架構:
// 引入 commentModel
const commentModel = require('../models/comment')
const commentController = {
add: (req, res, next) => {
const {username} = req.session
const {content} = req.body
if (!username) {
req.flash('errorMessage', '請先登入');
return next();
}
if (!content) {
req.flash('errorMessage', '缺少必要欄位');
return next();
}
// 若新增失敗就導回首頁
commentModel.add(username, content, err => {
return res.redirect('/');
})
}
}
// 輸出commentController
module.exports = commentController;
- 建立 commentModel 並新增 add,將表單提交內容寫入資料庫:
const commentModel = {
// 新增 comment 功能
add: (username, content, cb) => {
db.query(
'INSERT INTO comments(username, content) VALUES(?, ?)',
[username, content],
(err, results) => {
if (err) return cb(err);
cb(null);
}
);
}
}
Step4. 讀取留言功能
- 繼續在 commentModel 新增 getAll 來處讀取 username 資料,進行 users 和 comments 資料庫關聯:
// 讀取 comment 功能
getAll: (cb) => {
// 資料庫關聯
db.query(
`
SELECT U.nickname, C.content FROM comments as C
LEFT JOIN users as U on U.username = C.username
`,
(err, results) => {
if (err) return cb(err);
cb(null, results);
});
}
- 修改首頁 index 路由:
app.get('/', commentController.index)
- 在 Controller 處理 index 頁面,讀取資料並進行渲染:
// 在 index 頁面讀取資料
index: (req, res) => {
commentModel.getAll((err, results) => {
if (err) {
console.log(err);
}
res.render('index', {
comments: results;
});
});
}
- 修改首頁 index.ejs 顯示留言部分:
<% comments.forEach(function(comment) { %>
<div>
<h2><%= comment.nickname %> <%= comment.created_at %></h2>
<p><%= comment.content %></p>
</div>
<% }) %>
這樣其實就完成簡易的留言版了!但可以注意到,透過 EJS 語法輸出的時間,和我們預期的 'MMM-DD-YYYY'
格式不太相同:
補充:透過 moment.js 修改時間格式
透過 moment.js 這個套件,我們就能夠制定要輸出的時間日期格式,也能設定不同時區的時間。
- 安裝 moment.js 套件:
npm install moment --save
- 在 index.js 引入套件,並將 moment 和設定好的格式放入 locals,這樣就能在 View 使用:
// 引入 moment 套件
const moment = require('moment');
// 設定時間格式
const shortDateFormat = "YYYY-MM-DD HH:mm:ss";
// 將 moment 和 shortDateFormat 放到 locals 全域環境中
app.locals.moment = moment;
app.locals.shortDateFormat = shortDateFormat;
- 修改 index.ejs 的 View,用 moment 語法來輸出時間,也就是
moment('datetime').format('模版')
,改寫如下:
<% comments.forEach(function(comment) { %>
<div>
<h2><%= comment.nickname %></h2>
<p><%= moment(comment.created_at).format(shortDateFormat) %></p>
<p><%= comment.content %></p>
</div>
<% }) %>
這樣就成功把時間修改成 “YYYY-MM-DD h:mm:ss” 格式了!
找了幾種引用 moment 的寫法,最後覺得這篇:How to use node modules (like MomentJS) in EJS views? 內容蠻符合需要的,也就是要如何透過 node 提供的套件來改變 EJS views,稍微嘗試過後,也成功將時間格式調整成想要的樣子!
實戰:優化留言板
在上述範例中,我們在完成了一個有新增功能簡易留言版,接下來我們要試著把系統變得更完整一點。
Step1. 思考產品全貌
在開始實作產品內容之前,需要先思考幾個重點:
- 產品會有哪些功能:新增留言、編輯留言、刪除留言
- 如何設計路由
- 刪除留言
GET /delete_comments/:id => /
- 編輯留言
- 跳轉到編輯頁面
GET /update_comments/:id => form
- 編輯完成提交
POST /update_comments/:id => /
- 跳轉到編輯頁面
Step2. 規劃資料庫結構
規劃好系統功能和路由之後,再來就是規劃資料庫結構,這部分在先前的實作中就完成得差不多了。
在 users 和 comments table 中有個同樣的欄位 username,透過這個外鍵(Foreign Key),我們就能夠進行資料庫關聯,拿到想要的資料形式。
Step3. 實作刪除功能
- 在 index.ejs 新增刪除按紐,並且只有留言本人才看得到刪除鍵:
<% comments.forEach(function(comment) { %>
<div>
<h2><%= comment.nickname %></h2>
<p><%= moment(comment.created_at).format(shortDateFormat) %></p>
<p><%= comment.content %></p>
<!-- 判斷使用者的 username 和 comment 的 username 是否相同-->
<% if (username === comment.username) {%>
<!-- 只有本人才會看到刪除鍵 -->
<a href="/delete_comments/<%= comment.id %>">刪除</a>
<% } %>
</div>
<% }) %>
- 修改 commentModel 來拿取需要的資料,也就是 comment table 的 id 和 username:
// 讀取 comment 功能
getAll: (cb) => {
// 資料庫關聯
db.query(
`
SELECT U.nickname, U.created_at, C.content, C.id, C.username, C.created_at
FROM comments as C
LEFT JOIN users as U on U.username = C.username
ORDER BY C.created_at DESC
`,
(err, results) => {
if (err) return cb(err);
cb(null, results);
});
}
- 在 index.js 新增刪除留言的路由:
app.get('/delete_comments/:id', commentController.delete)
- 新增控制 delete 的方法。注意這裡的參數除了網址列上的 id,也需傳入 session 以確認是否為該 comment 作者::
delete: (req, res) => {
// 除了網址列上的 id,也需傳入 session 以確認是否為該 comment 作者
commentModel.delete(req.session.username, req.params.id, err => {
res.redirect('/');
})
}
- 實作 delete 的 commentModel 部分:
delete: (username, id, cb) => {
db.query(
`
DELETE FROM comments WHERE id=? AND username=?
`, [id, username],
(err, results) => {
if (err) return cb(err);
cb(null);
});
}
記得做好權限管裡,只有該則留言的’作者才能進行刪除:
Step4. 實作編輯功能
- 同樣先從 View 部分開始,在 index.ejs 新增編輯按紐:
<!-- 判斷使用者的 username 和 comment 的 username 是否相同-->
<% if (username === comment.username) {%>
<a href="/update_comments/<%= comment.id %>">編輯</a>
<a href="/delete_comments/<%= comment.id %>">刪除</a>
<% } %>
- 在 index.js 新增路由:
app.post('/comments', commentController.add, redirectBack)
app.get('/delete_comments/:id', commentController.delete)
// 讀取要編輯的 comment
app.get('/update_comments/:id', commentController.update)
// 執行修改 comment
app.post('/update_comments/:id', commentController.handleupdate)
- 在新增 Controller 控制相對應的路由前,因為 update 需要讀取相對應 id 的 comment,因此需要在 Model 新增讀取單一 comment 的功能:
// 讀取相對應 id 的 comment
get: (id, cb) {
// 資料庫關聯
db.query(
`
SELECT U.nickname, U.created_at, C.content, C.id, C.username, C.created_at
FROM comments as C
LEFT JOIN users as U on U.username = C.username
WHERE C.id = ?
`, [id],
(err, results) => {
if (err) return cb(err);
// 如果結果是 undefined 就會傳空物件,可避免程式出現錯誤
cb(null, results[0] || {});
}
);
},
- 接著就可以繼續編輯 Controller 中的 update:
update: (req, res) => {
commentModel.get(req.params.id, (err, result) => {
res.render('update', {
comment: result
});
});
},
- 再來實作 update 的 View 部分,因為直接修改網址就能進到這個頁面,要記得做驗證權限,必須是作者本人才能修改留言,這是透過前端處理:
<h1>編輯留言</h1>
<a href="/">返回</a>
<!-- 前端驗證 -->
<% if (username === comment.username) { %>
<form method="POST" action="/update_comments/<%= comment.id %>">
<textarea name="content" id="" cols="30" rows="10"></textarea>
<input type="submit" />
</form>
<% } else { %>
<div>ノンノンだよ!你沒有修改權限!</div>
<% } %>
- 有修改權限
- 沒有修改權限
- 最後剩下 handelUpdate 處理表單提交部分,首先是控制路由部分,透過比對 session 和 params.id 確認是否為本人,並傳入要修改的留言內容:
handleUpdate: (req, res) => {
// 後端驗證: 必須是本人才有權限修改
commentModel.update(req.session.username, req.params.id, req.body.content, err => {
res.redirect('/')
});
}
- 然後修改 Model 處理資料:
update: (username, id, content, cb) => {
db.query(
`
UPDATE comments SET content=? WHERE id=? AND username=?
`, [content, id, username],
(err, results) => {
if (err) return cb(err);
cb(null);
});
}
這樣就完成具有新增、刪除和編輯功能的留言板了!再來我們要學習如何美化頁面,也就是 EJS 如何修改 template 來改變畫面。
Step5. 美化頁面
透過 EJS 提供的語法 include()
我們可以建立模版,獨立出每個頁面都有的元素,例如 head 和 navbar 等等,如此就能簡化程式碼,範例如下:
<%- include('header'); -%>
<h1>
Title
</h1>
<p>
My page
</p>
<%- include('footer'); -%>
此外,include() 也可以傳入參數:
<ul>
<% users.forEach(function(user){ %>
<%- include('user/show', {user: user}); %>
<% }); %>
</ul>
認識新語法之後,就來繼續修改畫面吧!
- 首先在 index.ejs 加入 include() 語法,就像平常寫的 html 的架構。注意這裡的
<%- %>
是減號,代表會經過 html 解析:
<html>
<head>
<%- include('template/head') %>
</head>
<h1>簡易會員系統</h1>
<h2><%= errorMessage %></h2>
<% if(username) { %>
<div>Hello, <%= username + '!' %></div>
<a href="/logout">登出</a>
<form method="POST" action="/comments">
<textarea name="content" id="" cols="30" rows="10"></textarea>
<input type="submit" />
</form>
<% } else { %>
<a href="/register">註冊</a>
<a href="/login">登入</a>
<% } %>
<% comments.forEach(function(comment) { %>
<div>
<h2><%= comment.nickname %></h2>
<p class="card-subtitle text-muted"><%= moment(comment.created_at).format(shortDateFormat) %></p>
<p><%= comment.content %></p>
<% if (username === comment.username) {%>
<a href="/update_comments/<%= comment.id %>">編輯</a>
<a href="/delete_comments/<%= comment.id %>">刪除</a>
<% } %>
</div>
<% }) %>
</html>
- 再來編輯 head.ejs 部分,引入 Bootstrap 提供的樣式和 meta tags:
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
成功引入之後,就會發現畫面套用了 Bootstrap 提供的樣式:
- 再來是 navbar,同樣在 index.ejs 中加上 include() 語法:
<%- include('template/navbar') %>
- 新增模版 navbar.ejs,可在 Bootstrap/navbar 找合適的模版來套用,並且把之前在 index.ejs 做的登入驗證同樣放到這裡,加上對應的路徑:
<nav class="navbar navbar-dark bg-dark">
<a class="navbar-brand" href="/" >這是一個留言版</a>
<div>
<% if(username) { %>
<div class="">
<span class="text-light">Hello, <%= username + '!' %></span>
<a class="btn btn-outline-light my-2 my-sm-0" href="/logout">登出</a>
</div>
<% } else { %>
<a class="btn btn-outline-light my-2 my-sm-0" href="/login">登入</a>
<a class="btn btn-outline-warning my-2 my-sm-0" href="/register">註冊</a>
<% } %>
</div>
</nav>
效果會長這樣:
此外,像 head.ejs、navbar 這些通用模版,可放在 views/template 資料夾統一管理:
- 接著來修改註冊和登入頁面,同樣可在 Bootstrap 找合適的 forms 版型:
- register.ejs 註冊頁面
<!DOCTYPE html>
<html>
<head>
<%- include('../template/head') %>
</head>
<%- include('../template/navbar') %>
<div class="container">
<!-- 若沒有錯誤會回傳一個空陣列 [] -->
<% if (errorMessage.length > 0) { %>
<div class="alert alert-danger" role="alert">
<%= errorMessage %>
</div>
<% } %>
<form method="POST" action="/register">
<div class="form-group row">
<label class="col-sm-2 col-form-label">Username: </label>
<div class="col-sm-10">
<input class="form-control" name="username">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Nickname: </label>
<div class="col-sm-10">
<input class="form-control" name="nickname">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Password: </label>
<div class="col-sm-10">
<input class="form-control" name="password" type="password">
</div>
</div>
<button type="submit" class="btn btn-dark mb-2">Submit</button>
</form>
</div>
</html>
- login.ejs 登入頁面
<!DOCTYPE html>
<html>
<head>
<%- include('../template/head') %>
</head>
<%- include('../template/navbar') %>
<div class="container">
<!-- 若沒有錯誤會回傳一個空陣列 [] -->
<% if (errorMessage.length > 0) { %>
<div class="alert alert-danger" role="alert">
<%= errorMessage %>
</div>
<% } %>
<form method="POST" action="/login">
<div class="form-group row">
<label class="col-sm-2 col-form-label">Username: </label>
<div class="col-sm-10">
<input class="form-control" name="username">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">Password: </label>
<div class="col-sm-10">
<input class="form-control" name="password" type="password">
</div>
</div>
<button type="submit" class="btn btn-dark mb-2">Submit</button>
</form>
</div>
</html>
兩者寫法其實差不多,只有欄位和路徑需要修改,畫面效果如下:
- 再來是首頁 index.ejs,同樣幫 forms 還有留言卡套用樣式:
<!DOCTYPE html>
<html>
<head>
<%- include('template/head') %>
</head>
<%- include('template/navbar') %>
<div class="container mx-auto mt-3" style="width: 100%;">
<% if (errorMessage.length > 0) { %>
<div class="alert alert-danger" role="alert">
<%= errorMessage %>
</div>
<% } %>
<% if(username) { %>
<form method="POST" action="/comments">
<div class="form-group">
<div class="text-dark">留言內容</div>
<textarea name="content" class="form-control"></textarea>
</div>
<button type="submit" class="btn btn-dark">提交</button>
</form>
<% } else { %>
<div class="alert alert-warning" role="alert">留言請先登入</div>
<% } %>
<div class="card-container d-flex flex-wrap">
<% comments.forEach(function(comment) { %>
<div class="card m-2" style="width: 18rem; height: 16rem;">
<div class="card-header">
<h5 class="card-title"><%= comment.nickname %></h5>
<h6 class="card-subtitle text-muted"><%= moment(comment.created_at).format(shortDateFormat) %></h6>
<% if (username === comment.username) {%>
<a href="/update_comments/<%= comment.id %>" class="card-link">編輯</a>
<a href="/delete_comments/<%= comment.id %>" class="card-link">刪除</a>
<% } %>
</div>
<div class="card-body" style="overflow:scroll; overflow-x:hidden;">
<p class="card-text" ><%= comment.content %></p>
</div>
</div>
<% }) %>
</div>
</div>
</html>
效果如下:
- 最後是編輯留言頁面 update.ejs,也和其他頁面版型差不多:
<!DOCTYPE html>
<html>
<head>
<%- include('template/head') %>
</head>
<%- include('template/navbar') %>
<div class="container mx-auto mt-4" style="width: 100%;">
<!-- 若沒有錯誤會回傳一個空陣列 [] -->
<% if (errorMessage.length > 0) { %>
<div class="alert alert-danger" role="alert">
<%= errorMessage %>
</div>
<% } %>
<% if (username === comment.username) { %>
<form method="POST" action="/update_comments/<%= comment.id %>">
<div class="form-group">
<div class="text-dark">編輯留言</div>
<textarea name="content" class="form-control"></textarea>
</div>
<button type="submit" class="btn btn-dark">提交</button>
</form>
<% } else { %>
<div>ノンノンだよ!你沒有修改權限!</div>
<% } %>
</div>
</html>
透過這個範例,我們學到該如何使用 include() 語法來引入模板,如此就能簡化程式碼,將重複使用的區塊給模組化;此外,也透過 BootStrap 提供的現成樣式,讓我們能快速修改頁面,達到想要的畫面效果。
結語
實際跟著範例操作之後,會發現透過 MVC 模式撰寫程式,思考邏輯會很類似。使用 Express 框架實作網頁時,會依照下方流程進行:
- 思考產品全貌:哪些功能?哪些路由?
- 規劃資料庫結構
- 載入需要的模組,設定 app 路由部分
- 依照 MVC 架構撰寫程式碼:
- 設定 Controller:針對不同路由進行控制
- 設定 Model:如何處理資料
- 設定 View:如何呈現畫面
也藉這個機會來複習 BootStrap 使用方式,這和之前「先刻版面再加入功能」的模式正好相反,反而是先把功能做好,再套用現有樣式,這也是藉由 MVC 將畫面和功能分割開來才能夠辦到。
在學會如何寫出一個簡單的會員註冊系統和留言版之後,其實就能夠應用到各種網頁,期待之後能夠熟悉 MVC 架構的寫法!