本文為利用後端好朋友們 express & sequelize 實作出一個部落格的過程紀錄
零、內容摘要
1. 體驗從零開始的流程,並知道用 JS 寫後端跟用 PHP 的差異。
2. 思考產品全貌,規劃功能與路由
3. 規劃資料庫結構
4. 設置環境
5. 開始實作
a. 用了後端好朋友以後,models 裡面的東西就不用管了
b. 先把畫面實作出來
c. index.js 路由
d. 實作 controllers
6. 遇到的困難
a. 環境問題、Sequel Pro or XAMPP 跑不起來,真的很煩
b. debug 紀錄
7. 心得
用這個來寫後端,真的跟以前差非常多,有種痛快的感覺,覺得對後端改觀了,不再那麼討厭,好像開始有點理解它了。
不過,我要怎麼在一個專案裡面,同時用 react 以及 express 啊?就是 views 會被 react 完全取代,但是 controllers 還是會在是嗎?
一、思考部落格全貌
1. 需要的功能
a. 身為一個管理員,能登入以及登出
b. 身為一個管理員,能夠新增文章標題及內文
c. 身為一個管理員,能夠刪除文章
d. 身為一個管理員,能夠編輯文章標題及內文
e. 身為一個訪客,點擊文章列表可以看到所有文章
2. 思考路由設計
a. 登入、登出功能:只要有一組帳密就可以了
a.1 登入:
get: /login
post: /login → /
a.2 登出:get: /logout
b. CRUD 夥伴們
b.1 新增:post: /create_article/id → /
b.2 刪除:get: /delete_article/id → /
b.3 編輯:
get /edit_article/id → form
post /edit_article/id → /
二、規劃資料庫結構
0. id, username, password, title, content, UserId, createdAt, updatedAt
註:應該要有兩個資料庫才對,使用者以及文章
使用者:id, username, password, createdAt, updatedAt
文章:UserId, title, content, createdAt, updatedAt
註:本來規劃時寫的是 ArticleId,但這是錯的,要關聯資料的話,應該要用 UserId 才對,而且本來文章那個 table 裡面的 id 就是文章的 id 了。
1. 限制 username 長度
a. 在開資料庫時想好字元
b. 在註冊時提示,過長字串無法註冊
2. 思考 model 指令
id, createdAt, updatedAt 是自動生成的,在下指令時可以不用寫
使用者:$ npx sequelize-cli model:generate --name User --attributes username:string,password:string
文章:$ npx sequelize-cli model:generate --name Article --attributes UserId:integer,title:text,content:text
三、環境設置
註:一定要注意安裝順序喔,我在步驟三漏裝了 express,後來補裝有一些奇怪難解的錯誤訊息(require is not defined 之類的),是又再重頭安裝一次才變得正常。自動完成回來了,嗚嗚,有 prettier 真好。
1. $ npm init
2. $ npm install eslint prettier prettier-eslint
,接著再 eslint --init
3. $ npm install express
,接著再 $ npm install ejs body-parser express-session connect-flash bcrypt
4. $ npm install --save sequelize mysql2
5. $ npm install --save-dev sequelize-cli
6. $ npx sequelize-cli init
7. 改 config 檔案 development
8. 建立 models: user & article
使用者:$ npx sequelize-cli model:generate --name User --attributes username:string,password:string
文章:$ npx sequelize-cli model:generate --name Article --attributes UserId:integer,title:text,content:text
9. Running Migrations
$ npx sequelize-cli db:migrate
註:到這裡後,資料庫裡面應該會有剛剛建的兩個表格了
10. models 加上關聯
static associate(models) {
User.hasMany(models.Article)
}
static associate(models) {
Article.belongsTo(models.User)
}
四、實作部落格功能
1. 事前規劃
a. 登入機制,管理員可以登入
b. 可以新增、編輯、刪除文章
如果該使用者沒有編輯權限,就把它轉到首頁
2. 實作過程
a. index.js 開始基本設定
const express = require('express')
const session = require('express-session')
const flash = require('connect-flash')
const bodyParser = require('body-parser')
const app = express()
const port = 5001
const userController = require('./controllers/user')
const articleController = require('./controllers/article')
app.set('view engine', 'ejs')
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
}))
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.use(flash())
app.use((req, res, next) => {
res.locals.username = req.session.username
res.locals.errorMessage = req.flash('errorMessage')
next()
})
app.get('/', articleController.index)
// 這個函式用在顯示錯誤訊息後導回同一頁,好用必用。
function redirectBack(req, res) {
res.redirect("back")
}
app.listen(port, () => {
console.log('success')
})
b. controllers > user.js
有 login、handleLogin、logout
// controllers > user.js
const bcrypt = require("bcrypt");
const db = require("../models");
const User = db.User;
const userController = {
login: (req, res) => {
res.render("user/login");
},
handleLogin: (req, res, next) => {
const { username, password } = req.body;
if (!username || !password) {
req.flash("errorMessage", "Please fill in all the required fields.");
return next();
}
User.findOne({
where: {
username,
},
})
.then((user) => {
if (!user) {
req.flash("errorMessage", "invalid user");
return next();
}
bcrypt.compare(password, user.password, function (err, isSuccess) {
if (err || !isSuccess) {
req.flash("errorMessage", "invalid password");
return next();
}
req.session.username = user.username;
req.session.userId = user.id;
res.redirect("/");
});
})
.catch((err) => {
req.flash("errorMessage", err.toString());
return next();
});
},
logout: (req, res) => {
req.session.username = null;
res.redirect("/");
},
};
module.exports = userController;
c. views > template > head.ejs & navbar.ejs 建立結構
views
template
head.ejs
navbar.ejs
user
- login.ejs
add.ejs
edit.ejs
index.ejs
先從 navbar 開始做畫面
// head.ejs
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
// navbar.ejs
<nav class="navbar navbar-light bg-light"></nav>
<div class="container d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<a class="navbar-brand fs-3" href="/">Blog</a>
<ul class="nav">
<% if (username) { %>
<li class="nav-item">
<a class="nav-link" href="/new-post">New Post</a>
</li>
<% } %>
</ul>
</div>
<div>
<% if (username) { %>
<a class="btn btn-outline-danger" href="/logout">Logout</a>
<% } else { %>
<a class="btn btn-outline-dark" href="/login">Login</a>
<% } %>
</div>
</div>
</nav>
d. login.ejs
// login.ejs
<!doctype html>
<head>
<%- include('../template/head') %>
</head>
<html>
<%- include('../template/navbar')%>
<div class="container">
<% if (errorMessage && errorMessage.length > 0) { %>
<div class="alert alert-danger mt-3" role="alert">
<h5><%= errorMessage %></h5>
</div>
<% } %>
</div>
<div class="container py-3 px-3 my-4 rounded border border-primary border-2" style="width:400px;">
<form class="mt-3" method="POST" action="/login">
<div class="mb-3 row g-3 justify-content-center align-items-center">
<div class="col-auto">
<label class="col-form-label fs-4">Username: </label>
</div>
<div class="col-auto">
<input type="text" name="username" class="form-control">
</div>
</div>
<div class="mb-3 row g-3 justify-content-center align-items-center">
<div class="col-auto">
<label class="col-form-label fs-4">Password: </label>
</div>
<div class="col-auto">
<input type="password" name="password" class="form-control">
</div>
</div>
<div class="d-grid justify-content-end me-3">
<button type="submit" class="my-2 btn btn-primary">Submit</button>
</div>
</form>
</div>
</html>
e. views > new-post.ejs
<!doctype html>
<head>
<%- include('template/head') %>
</head>
<html>
<%- include('template/navbar')%>
<div class="container">
<% if (errorMessage && errorMessage.length > 0) { %>
<div class="alert alert-danger mt-3" role="alert">
<h5><%= errorMessage %></h5>
</div>
<% } %>
</div>
<div class="container-fluid mx-auto align-items-center justify-content-center" style="width:700px;">
<% if (username) { %>
<h3 class="mt-3">Would you like to make a new post?</h3>
<form action="/new-post" method="POST">
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<div style="width:600px;">
<input type="text" name="title" class="form-control">
</div>
</div>
<div>
<textarea class="px-2 py-2" name="content" cols="74" rows="10"></textarea>
</div>
<button class="btn btn-primary mt-3" type="submit">Submit</button>
</form>
<% } %>
</div>
</html>
f. controllers > article.js
有 index、add、delete、edit、handleEdit
// controllers > article.js
const db = require("../models");
const Article = db.Article;
const User = db.User;
const articleController = {
newPost: (req, res) => {
res.render("new-post");
},
handleNewPost: (req, res, next) => {
const { userId } = req.session;
const { title, content } = req.body;
if (!title || !content) {
req.flash("errorMessage", "Please fill in all the required fields.");
return next();
}
Article.create({
title,
content,
UserId: userId,
})
.then(() => {
res.redirect("/");
})
.catch((err) => {
req.flash("errorMessage", err.toString());
return next();
});
},
index: (req, res) => {
Article.findAll({
include: User,
order: [["updatedAt", "DESC"]],
})
.then((articles) => {
res.render("index", {
articles,
});
})
.catch((err) => {
req.flash("errorMessage", err.toString());
return next();
});
},
delete: (req, res) => {
Article.findOne({
where: {
id: req.params.id,
UserId: req.session.userId,
},
})
.then((article) => {
return article.destroy();
})
.then(() => {
res.redirect("/");
})
.catch((err) => {
req.flash("errorMessage", err.toString());
return next();
});
},
edit: (req, res) => {
Article.findOne({
where: {
id: req.params.id,
},
})
.then((article) => {
res.render("edit", {
article,
});
})
.catch((err) => {
req.flash("errorMessage", err.toString());
return next();
});
},
handleEdit: (req, res, next) => {
const { title, content } = req.body;
if (!title || !content) {
req.flash("errorMessage", "Please fill in all the required fields.");
return next();
}
Article.findOne({
where: {
id: req.params.id,
UserId: req.session.userId,
},
})
.then((article) => {
return article.update({
title: req.body.title,
content: req.body.content,
});
})
.then(() => {
res.redirect("/");
})
.catch((err) => {
req.flash("errorMessage", err.toString());
return next();
});
},
};
module.exports = articleController;
五、Bootstrap 美化頁面
c.1 Starter template 把 head 裡面資訊拿進來
c.2 navbar 要對齊 <div class="container d-flex justify-content-between align-items-center"></div>
c.3 置中目前用的方式是「加上指定寬度,然後用上面兩個對齊方法」
六、實作中遇到的困難
1. 有時候 Sequel Pro 怎麼連都連不上去,有夠煩的;不然就是 XAMPP 有問題,這時重開電腦是一個好選擇。不然就是做其他的作業,像是作業三簡答題之類的,不需要 database 那種,如果真的不行,就用 phpmyadmin 吧。
2. 不要太執著於美化頁面,功能面 >>>> 美觀。
3. 原來 res.redirect("back");
是用在「顯示錯誤訊息以後,把使用者導回原來那一頁」,真的好用。
4. 用了後端好朋友系列,models 完全不用管耶;只要在乎下面三個就好
index.js: 路由
controllers
views