本文為利用後端框架 express 實作抽獎網站的過程,包含 express + sequelize 的基本設置、規劃前台與後台畫面及路由、獎項的 API 與抽獎機率實作,最後把網站部署到 Heroku 上面
零、內容摘要
0. 實作前沒有什麼想法,很苦惱
只要做抽獎頁面就好,我原以為是整個餐廳網站都要全部改成 express,仔細看作業說明,就是做一個抽獎的網站,改寫是下一週的作業。
剛開始還是因為沒有畫面,所以不知道該做什麼,而不知道該做什麼的時候,就連問題在哪都不清楚,自然卡東卡西的,「只要定義清楚問題,自然就會有答案」又再次驗證了這句話。
1. 把需要做的功能寫下來拆解,好像逐漸有了雛形
a. 使用 express + sequelize 先把抽獎網站雛形做出來,包含前台與後台頁面
b. 實作新增、讀取、刪除、編輯功能
c. 實作 API
d. 實作機率
e. 部署
2. 當按下抽獎按鈕時,就會出現獎項。獎項會有獎品名稱、內容說明、以及獎品圖片
a. 按下按鈕,清空畫面
b. 去呼叫 API 回傳獎項
3. API 其實就是在 controller 裡的一個 method
仔細想想,API 就是一份資料嘛,在 method 裡面可以回傳資料,格式大概會長這樣:
{"id":1,"item":"Fourth Prize","url":"https://images.pexels.com/photos/3945317/pexels-photo-3945317.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260","content":"Movie tickets"}
controller 裡面的 api method 最終的目的就是「顯示某資料庫裡面的一個獎項」,而這個「顯示的計算過程」就是機率。
在增加每個獎項時,權重可以寫成 1-100 的正整數,這個正整數就等於是百分比
什麼是權重?就是希望一件事情發生的機率,例如說,權重的範圍是 1-100,權重數字越大則代表發生的機率越大,因為是機率,所以直接設定百分比是最快的。
a. 提供資料庫裡面其中一個獎項
b. 根據得獎機率,計算顯示獎項的機率
一、思考抽獎網站全貌
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 → /
c. API
get: /api
二、規劃資料庫結構
1. 獎品資料庫:item, content, url, ratio
2. 使用者資料庫:username, password
3. model 指令:Data Types
獎品:
$ npx sequelize-cli model:generate --name Prize --attributes item:text,content:text,url:text,ratio:integer
使用者:
$ npx sequelize-cli model:generate --name User --attributes username:string,password:string
三、環境設置
註:安裝 eslint 那些的部分,參考 EsLint 我們重頭來過吧,如果已經 init 過了,就不需要再一次了,因此我從步驟三開始。
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 & prize
獎品:
$ npx sequelize-cli model:generate --name Prize --attributes item:text,content:text,url:text,ratio:integer
使用者:
$ npx sequelize-cli model:generate --name User --attributes username:string,password:string
9. Running Migrations
$ npx sequelize-cli db:migrate
註:到這裡後,資料庫裡面應該會有剛剛建的兩個表格了
四、實作抽獎網站功能
參考 用後端好朋友們 express & sequelize 實作部落格
實作可以分成三個部分:
1. express + sequelize 基本建置
2. 規劃頁面及路由
3. 實作前台與後台頁面還有 CRUD 功能
4. 實作 API & 抽獎機率
5. 難題紀錄
a. bootstrap 圖片置中對齊 class="w-50 rounded mx-auto d-block"
b. 載入靜態檔案,要在 index.js 加上這行 app.use(express.static(`${__dirname}/public`));
6. 檔案結構及內容
controllers folder
prize.js api 的結果以及要做的事,也寫在這裡
user.js
public folder
- api.js 按下抽獎按鈕時,清空網頁並且呼叫 API 顯示抽獎結果
views folder
template
head.ejs 引入 bootstrap
navbar.ejs 導覽列
user
- login.ejs 顯示登入頁面
add.ejs 顯示新增獎項頁面
cms.ejs 顯示後台所有獎項頁面
edit.ejs 顯示編輯獎項頁面
index.ejs 負責顯示抽獎前台頁面
index.js: 負責基本設定及路由
// index.js
const express = require("express");
const bodyParser = require("body-parser");
const session = require("express-session");
const flash = require("connect-flash");
const app = express();
const port = 5001;
const userController = require("./controllers/user");
const prizeController = require("./controllers/prize");
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(express.static(`${__dirname}/public`));
app.use((req, res, next) => {
res.locals.username = req.session.username;
res.locals.errorMessage = req.flash("errorMessage");
next();
});
app.get("/", prizeController.index);
function redirectBack(req, res) {
res.redirect("back");
}
app.get("/api", prizeController.api);
app.get("/login", userController.login);
app.post("/login", userController.handleLogin, redirectBack);
app.get("/logout", userController.logout);
app.get("/cms", prizeController.cms);
app.get("/add", prizeController.add);
app.post("/add", prizeController.handleNewPrize, redirectBack);
app.get("/delete_prize/:id", prizeController.delete);
app.get("/edit_prize/:id", prizeController.edit);
app.post("/edit_prize/:id", prizeController.handleEdit, redirectBack);
app.listen(port, () => {
console.log("success");
});
// controllers > prize.js
const db = require("../models");
const { Prize } = db;
const prizeController = {
cms: async (req, res, next) => {
try {
const prizes = await Prize.findAll({
order: [["id", "ASC"]],
});
res.render("cms", {
prizes,
});
} catch (err) {
req.flash("errorMessage", err.toString());
return next();
}
},
add: (req, res) => {
res.render("add");
},
handleNewPrize: async (req, res, next) => {
const { item, content, url, ratio } = req.body;
if (!item || !content || !url || !ratio) {
req.flash("errorMessage", "Please fill in all the required fields.");
return next();
}
if (isNaN(Number(ratio)) || ratio < 1 || ratio > 100) {
req.flash("errorMessage", "Please fill in ratio with 1 to 100.");
return next();
}
try {
await Prize.create({
item,
content,
url,
ratio,
});
res.redirect("/cms");
} catch (err) {
req.flash("errorMessage", err.toString());
return next();
}
},
index: async (req, res, next) => {
try {
const prizes = await Prize.findAll({
order: [["id", "ASC"]],
});
res.render("index", {
prizes,
});
} catch (err) {
req.flash("errorMessage", err.toString());
return next();
}
},
delete: async (req, res, next) => {
try {
const prize = await Prize.findOne({
where: {
id: req.params.id,
},
});
await prize.destroy();
res.redirect("/cms");
} catch (err) {
req.flash("errorMessage", err.toString());
return next();
}
},
edit: async (req, res, next) => {
try {
const prize = await Prize.findOne({
where: {
id: req.params.id,
},
});
res.render("edit", {
prize,
});
} catch (err) {
req.flash("errorMessage", err.toString());
return next();
}
},
handleEdit: async (req, res, next) => {
const { item, content, url, ratio } = req.body;
if (!item || !content || !url || !ratio) {
req.flash("errorMessage", "Please fill in all the required fields.");
return next();
}
if (isNaN(Number(ratio)) || ratio < 1 || ratio > 100) {
req.flash("errorMessage", "Please fill in ratio with 1 to 100.");
return next();
}
try {
const prize = await Prize.findOne({
where: {
id: req.params.id,
},
});
await prize.update({
item,
content,
url,
ratio,
});
res.redirect("/cms");
} catch (err) {
req.flash("errorMessage", err.toString());
return next();
}
},
api: async (req, res) => {
try {
const drawResult = await getDraw();
const randomNumber = Math.floor(Math.random() * 100);
const id = drawResult[randomNumber];
const data = await Prize.findOne({
where: { id },
attributes: { include: ["item", "content", "url"] },
});
res.send(data);
} catch (err) {
req.flash("errorMessage", err.toString());
return next();
}
},
};
async function getDraw() {
const totalRatio = await Prize.sum("ratio");
const allPrizes = await Prize.findAll();
let countRatio = [];
for (const prize of allPrizes) {
const ratio = (prize.ratio / totalRatio).toFixed(2) * 100;
countRatio.push({
id: prize.id,
ratio,
});
}
let result = [];
for (let i = 0; i < countRatio.length; i++) {
for (let j = 1; j <= countRatio[i].ratio; j++) {
result.push(countRatio[i].id);
}
}
return result;
}
module.exports = prizeController;
// controllers > user.js
const bcrypt = require("bcrypt");
const db = require("../models");
const { User } = db;
const userController = {
login: (req, res) => {
res.render("user/login");
},
handleLogin: async (req, res, next) => {
const { username, password } = req.body;
if (!username || !password) {
req.flash("errorMessage", "Please fill in all the required fields.");
return next();
}
try {
let user = await User.findOne({
where: {
username,
},
});
if (!user) {
req.flash("errorMessage", "invalid user or password");
return next();
}
bcrypt.compare(password, user.password, (err, isSuccess) => {
if (err || !isSuccess) {
req.flash("errorMessage", "invalid user or password");
return next();
}
req.session.username = user.username;
res.redirect("/");
});
} catch (err) {
req.flash("errorMessage", err.toString());
return next();
}
},
logout: (req, res) => {
req.session.destroy();
res.redirect("/");
},
};
module.exports = userController;
// public > api.js
const mainHTML = document.querySelector(".wrapper");
document.querySelector(".draw-btn").addEventListener("click", () => {
mainHTML.innerHTML = "";
drawResult();
});
async function getDrawAPI() {
try {
const res = await fetch("/api");
// 我原本沒有宣告 draw 就不小心產生了一個全域變數
return (draw = await res.json());
// 寫這樣比較好
const draw = await res.json();
return draw;
} catch (err) {
return console.log(`Error: ${draw.error} & ${draw.message}`);
}
}
async function drawResult() {
try {
const result = await getDrawAPI();
mainHTML.innerHTML = `
<div class="wrapper">
<div class="text-center">
<h3 class="mt-3">${result.item}</h3>
<p>${result.content}</p>
</div>
<img class="w-50 rounded mx-auto d-block img-fluid" src="${result.url}">
</div>
`;
} catch (err) {
return console.log(`Error: ${result.error} & ${result.message}`);
}
}