本篇為 [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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const db = require ('../db' );const userModel = { 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 ); } ); }, 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 部分
先建立好程式碼架構,再來填寫路由部分:
1 2 3 4 5 6 7 8 9 10 11 const userModel = require ('../models/todo' )const userController = { get : (req, res ) => { } } module .exports = userController
在 index.js 引入 userController,並新增 userController 處理 login 和 logout 的路由:
1 2 3 4 5 const userController = require ('./controllers/user' )app.get ('/login' , userController.login ) app.post ('/login' , userController.handleLogin ) app.get ('/logout' , userController.logout )
Controller 就會根據不同路由,建立相對應的 Method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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.' ) } }, logout :: (req, res ) => { req.session .isLogin = false ; res.redirect ('/' ); } } module .exports = userController
Step4. 實作 View 部分
在根目錄增加一個首頁 index 的路由:
1 2 3 4 app.get ('/' , (req, res ) => { res.render ('index' ) })
接著建立 index.ejs 檔案,實作主要頁面:
1 2 3 4 5 <h1>簡易會員系統</h1> <a href ="/register" > 註冊</a > <a href ="/login" > 登入</a > <a href ="/logout" > 登出</a >
到這裡可先運行程式看看有沒有問題:
加入前面範例中的驗證功能:
1 2 3 4 5 6 7 8 <h1>簡易會員系統</h1> <% if (isLogin) { %> <a href ="/logout" > 登出</a > <% } else { %> <a href ="/register" > 註冊</a > <a href ="/login" > 登入</a > <% } %>
這樣在未登入狀態下,就只會看到註冊和登入,這樣就完成了首頁 index 的部分:
Step5. 實作註冊功能
先建立 register 路由:
1 2 app.get ('/register' , userController.register ) app.post ('/register' , userController.handleRegister )
根據不同路由建立相對應的 controller,這裡可以把有關 user 的 view 放在同一個資料夾,較方便管理:
1 2 3 4 5 6 7 8 9 10 register : (req, res ) => { res.render ('user/register' ); }, handleRegister : (req, res ) => {},
新增 register 的 view:
1 2 3 4 5 6 7 8 9 10 <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 部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 handleRegister : (req, res ) => { const {username, password, nickname} = req.body ; if (!username || !password || !nickname) { return req.flash ('errorMessage' , '缺少必要欄位' ); } userModel.add ({ username, nickname, password }, (err ) => { if (err) { return req.flash ('errorMessage' , err.toString ()); } } req.session .username = username; res.redirect ('/' ); }); }, logout : (req, res ) => { req.session .username = null ; res.redirect ('/' ); }
但這樣其實就要把 session 改成存 username,有 username 就代表有登入狀態:
1 2 3 4 5 6 7 app.use ((req, res, next ) => { res.locals .username = req.session .username res.locals .errorMessage = req.flash ('errorMessage' ) next () })
首頁 index.ejs 的驗證也同樣修改成判斷是否有 username,因為使用 <%= %> 會輸出原始碼,相當於做 escpae:
1 2 3 4 5 6 7 8 9 10 11 <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 套件,即可將明碼進行雜湊處理,避免儲存明碼造成安全性問題:
1 2 3 4 const bcrypt = require ('bcrypt' );const saltRounds = 10 ;
1 2 3 bcrypt.hash (myPlaintextPassword, saltRounds, function (err, hash ) { });
再來修改剛才的 handleRegister,利用 bcrypt 套件對密碼進行雜湊處理,再儲存 hash 過的密碼到資料庫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 handleRegister : (req, res, next ) => { const {username, password, nickname} = req.body ; if (!username || !password || !nickname) { req.flash ('errorMessage' , '缺少必要欄位' ); return next (); } bcrypt.hash (password, saltRounds, function (err, hash ) { if (err) { req.flash ('errorMessage' , err.toString ()); return next (); } userModel.add ({ username, nickname, password : hash }, (err ) => { if (err) { req.flash ('errorMessage' , '已存在相同用戶名' ); return next (); } req.session .username = username; res.redirect ('/' ); }); }); },
執行程式並註冊一個帳號,確認資料是否有成功寫進資料庫,密碼也有先經過 hash 才儲存:
這樣就差不多完成了註冊功能:
Step7. 實作登入功能
再來要修改登入頁面 login.ejs,畫面和註冊頁面很類似:
1 2 3 4 5 6 7 8 9 <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() 驗證雜湊密碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 login : (req, res ) => { res.render ('login' ) }, handleLogin : (req, res, next ) => { const { username, password } = req.body ; if (!username || !password) { req.flash ('errorMessage' , '請輸入您的帳密!' ); return next (); } 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 ('/' ) }); }) }, logout : (req, res ) => { req.session .username = null ; res.redirect ('/' ); }
在登入和註冊頁面,每當呼叫 next 時,就會將控制權給下一個中間介,可以自己寫一個中間介 redirectBack,用來在提交表單後自動導回上一頁:
1 2 3 4 5 6 7 8 9 10 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 介面新增留言表單,如果是登入狀態,就能看到留言區塊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <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. 新增留言功能
新增路由:
1 2 3 4 const commentController = require ('./controllers/comment' );app.post ('/comments' , commentController.add , redirectBack)
建立 commentConrtoller 架構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 ('/' ); }) } } module .exports = commentController;
建立 commentModel 並新增 add,將表單提交內容寫入資料庫:
1 2 3 4 5 6 7 8 9 10 11 12 13 const commentModel = { 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 資料庫關聯:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 路由:
1 app.get ('/' , commentController.index )
在 Controller 處理 index 頁面,讀取資料並進行渲染:
1 2 3 4 5 6 7 8 9 10 11 index : (req, res ) => { commentModel.getAll ((err, results ) => { if (err) { console .log (err); } res.render ('index' , { comments : results; }); }); }
修改首頁 index.ejs 顯示留言部分:
1 2 3 4 5 6 <% 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 套件:
1 npm install moment --save
在 index.js 引入套件,並將 moment 和設定好的格式放入 locals,這樣就能在 View 使用:
1 2 3 4 5 6 7 const moment = require ('moment' );const shortDateFormat = "YYYY-MM-DD HH:mm:ss" ;app.locals .moment = moment; app.locals .shortDateFormat = shortDateFormat;
修改 index.ejs 的 View,用 moment 語法來輸出時間,也就是 moment('datetime').format('模版'),改寫如下:
1 2 3 4 5 6 7 <% 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 新增刪除按紐,並且只有留言本人才看得到刪除鍵:
1 2 3 4 5 6 7 8 9 10 11 12 <% 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 新增刪除留言的路由:
1 app.get ('/delete_comments/:id' , commentController.delete )
新增控制 delete 的方法。注意這裡的參數除了網址列上的 id,也需傳入 session 以確認是否為該 comment 作者::
1 2 3 4 5 6 delete : (req, res ) => { commentModel.delete (req.session .username , req.params .id , err => { res.redirect ('/' ); }) }
實作 delete 的 commentModel 部分:
1 2 3 4 5 6 7 8 9 10 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 新增編輯按紐:
1 2 3 4 5 <!-- 判斷使用者的 username 和 comment 的 username 是否相同--> <% if (username === comment.username) {%> <a href="/update_comments/<%= comment.id %>">編輯</a> <a href="/delete_comments/<%= comment.id %>">刪除</a> <% } %>
在 index.js 新增路由:
1 2 3 4 5 6 app.post ('/comments' , commentController.add , redirectBack) app.get ('/delete_comments/:id' , commentController.delete ) app.get ('/update_comments/:id' , commentController.update ) app.post ('/update_comments/:id' , commentController.handleupdate )
在新增 Controller 控制相對應的路由前,因為 update 需要讀取相對應 id 的 comment,因此需要在 Model 新增讀取單一 comment 的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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); cb (null , results[0 ] || {}); } ); },
接著就可以繼續編輯 Controller 中的 update:
1 2 3 4 5 6 7 update : (req, res ) => { commentModel.get (req.params .id , (err, result ) => { res.render ('update' , { comment : result }); }); },
再來實作 update 的 View 部分,因為直接修改網址就能進到這個頁面,要記得做驗證權限,必須是作者本人才能修改留言,這是透過前端處理:
1 2 3 4 5 6 7 8 9 10 11 12 13 <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 確認是否為本人,並傳入要修改的留言內容:
1 2 3 4 5 6 handleUpdate : (req, res ) => { commentModel.update (req.session .username , req.params .id , req.body .content , err => { res.redirect ('/' ) }); }
然後修改 Model 處理資料:
1 2 3 4 5 6 7 8 9 10 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 等等,如此就能簡化程式碼,範例如下:
1 2 3 4 5 6 7 8 <%- include('header'); -%> <h1> Title </h1> <p> My page </p> <%- include('footer'); -%>
此外,include() 也可以傳入參數:
1 2 3 4 5 <ul> <% users.forEach(function(user){ %> <%- include('user/show', {user: user}); %> <% }); %> </ul>
認識新語法之後,就來繼續修改畫面吧!
首先在 index.ejs 加入 include() 語法,就像平常寫的 html 的架構。注意這裡的 <%- %> 是減號,代表會經過 html 解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <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:
1 2 3 4 5 6 7 <!-- 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() 語法:
1 <%- include('template/navbar') %>
新增模版 navbar.ejs,可在 Bootstrap/navbar 找合適的模版來套用,並且把之前在 index.ejs 做的登入驗證同樣放到這裡,加上對應的路徑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <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 版型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!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>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <!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 還有留言卡套用樣式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <!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,也和其他頁面版型差不多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <!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 架構的寫法!