以下是登入和註冊頁面的wireframe,樣式細節可依個人喜好調整
可以看到上面兩個圖的外層樣式是一樣的,只有input不一樣,此時我們就可以製作共用的樣式元件,來避免重複的code寫了兩次,也順便讓登入頁和註冊頁的樣式能統一,因為tailwindCSS的class會變得很零碎,如果分開維護,很有可能兩個頁面的樣式會不一致。
建立共用元件
在components/UI
底下新增Card.js
//Card.js
import logoImage from '../../assets/logo-black.svg';
const Card = ({ children }) =>{
return <div className="w-screen h-screen bg-slate-900">
<div className="w-[480px] mx-auto py-12 px-16 absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 bg-white rounded shadow-xl">
<div className="flex items-center mb-6 justify-center">
<img className="w-[30px] h-auto" src={logoImage} alt="logo" />
<h1 className="text-3xl ml-2 font-bold">BLOG DEV</h1>
</div>
{children}
</div>
</div>
}
export default Card;
🔺補充資訊:children
這個props可以用於定義可插入的内容區域,允許父元件傳遞任意的子元素到這個區域。詳細用法請見react children
然後到pages
資料夾下新增Login.js
進行開發
import { useState } from "react";
import Card from "../components/UI/Card"; //引入剛剛建立的Card
const LoginPage = (props) => {
//email欄位值
const [email, setEmail] = useState("");
//email touched值
const [emailTouched, setEmailTouched] = useState(false);
//password欄位值
const [password, setPassword] = useState("");
//password touched欄位值
const [passwordTouched, setPasswordTouched] = useState(false);
//email的檢核
const emailIsValid = email.trim() !== "";
const emailInputIsInValid = !emailIsValid && emailTouched;
//password的檢核
const passwordIsValid = password.trim() !== "";
const passwordInputIsInValid = !passwordIsValid && passwordTouched;
//表單的狀態
let formIsValid = false;
if (emailIsValid && passwordIsValid) {
formIsValid = true;
}
//當email input值變更時做的處理
const handleEmailInputChange = (event) => {
setEmailTouched(true);
setEmail(event.target.value);
};
//當email input值blur時做的處理
const handleEmailInputBlur = (event) => {
setEmailTouched(true);
};
//當password input值變更時做的處理
const handlePasswordInputChange = (event) => {
setPassword(event.target.value);
setPasswordTouched(true);
};
//當password input值blur時做的處理
const handlePasswordInputBlur = (event) => {
setPasswordTouched(true);
};
//處理送出表單
const handleSubmit = (event) => {
if (!formIsValid) return;
event.preventDefault();
//呼叫登入API
};
//根據emailInput是否valid來顯示對應樣式
const emailInputClasses = emailInputIsInValid
? "border-red-300 focus:ring-red-500"
: "border-slate-300 focus:ring-sky-500";
//根據passwordInput是否valid來顯示對應樣式
const passwordInputClasses = passwordInputIsInValid
? "border-red-300 focus:ring-red-500"
: "border-slate-300 focus:ring-sky-500";
return (
<Card>
<form className="w-full" onSubmit={handleSubmit}>
<div className="mb-4">
<label htmlFor="email" className="custom-font">Email</label>
<input
id="email"
type="text"
placeholder="請輸入Email"
value={email}
onBlur={handleEmailInputBlur}
onChange={handleEmailInputChange}
className={`mt-1 block w-full px-3 py-2 bg-white border rounded-md text-sm
shadow-sm placeholder-slate-400 focus:outline-none focus:ring-1
${emailInputClasses}`}
/>
{!emailInputIsInValid || (
<p className="text-red-500 text-sm">帳號為必填欄位</p>
)}
</div>
<div>
<label htmlFor="password" className="custom-font">Password</label>
<input
id="password"
type="text"
placeholder="請輸入密碼"
value={password}
onBlur={handlePasswordInputBlur}
onChange={handlePasswordInputChange}
className={`mt-1 block w-full px-3 py-2 bg-white border rounded-md text-sm
shadow-sm placeholder-slate-400 focus:outline-none focus:ring-1
${passwordInputClasses}`}
/>
{!passwordInputIsInValid || (
<p className="text-red-500 text-sm">密碼為必填欄位</p>
)}
</div>
<button className="mt-8 px-4 py-2 bg-violet-600 hover:bg-violet-700 duration- 200 text-white w-full rounded cursor-pointer">
Sign In
</button>
<div className="flex justify-center text-sm py-4">
<p className="text-gray-400">Don't have an account?</p>
<button className="ml-2 duration-200 text-violet-600 cursor-pointer">
Sign Up
</button>
</div>
</form>
</Card>
);
};
export default LoginPage;
接續處理handleSubmit
裡面的內容,當我們輸入好帳號和密碼時,就要點擊按鈕呼叫login的api。
首先在src
資料夾底下新增api
資料夾並在其底下新增api.js
//api.js
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:5000/api', //後端API的URL
});
export default api;
再回到Login.js
import { useState } from "react";
import { Link } from "react-router-dom";
import Card from "../components/UI/Card";
import api from "../api/api"; //引入剛剛建立的api
const LoginPage = (props) => {
//email欄位值
const [email, setEmail] = useState("");
//email touched值
const [emailTouched, setEmailTouched] = useState(false);
//password欄位值
const [password, setPassword] = useState("");
//password touched欄位值
const [passwordTouched, setPasswordTouched] = useState(false);
//email的檢核
const emailIsValid = email.trim() !== "";
const emailInputIsInValid = !emailIsValid && emailTouched;
//password的檢核
const passwordIsValid = password.trim() !== "";
const passwordInputIsInValid = !passwordIsValid && passwordTouched;
//表單的狀態
let formIsValid = false;
if (emailIsValid && passwordIsValid) {
formIsValid = true;
}
//當email input值變更時做的處理
const handleEmailInputChange = (event) => {
setEmailTouched(true);
setEmail(event.target.value);
};
//當email input值blur時做的處理
const handleEmailInputBlur = (event) => {
setEmailTouched(true);
};
//當password input值變更時做的處理
const handlePasswordInputChange = (event) => {
setPassword(event.target.value);
setPasswordTouched(true);
};
//當password input值blur時做的處理
const handlePasswordInputBlur = (event) => {
setPasswordTouched(true);
};
//處理送出表單
const handleSubmit = (event) => {
// 如果表單無效,則不執行後續操作
if (!formIsValid) return;
// 阻止預設的表單提交行為,以便透過 AJAX 請求提交數據
event.preventDefault();
// 建立使用者輸入的資料物件
const userData = {
email,
password,
};
// 使用 API 發送 POST 請求
api
.post("/auth/login", userData)
.then((result) => {
// 如果請求成功,將token存到localStorage裡面(當登入成功後後端會回傳token)
localStorage.setItem("user", JSON.stringify(result));
})
.catch((error) => {
console.error(error);
});
};
//根據emailInput是否valid來顯示對應樣式
const emailInputClasses = emailInputIsInValid
? "border-red-300 focus:ring-red-500"
: "border-slate-300 focus:ring-sky-500";
//根據passwordInput是否valid來顯示對應樣式
const passwordInputClasses = passwordInputIsInValid
? "border-red-300 focus:ring-red-500"
: "border-slate-300 focus:ring-sky-500";
return (
<Card>
(略...)
</Card>
);
};
export default LoginPage;
🔺補充資訊:handleSubmit
的event.preventDefault()
是用來阻止表單的預設提交行為。因為當我們在HTML表單中按下提交按鈕時,瀏覽器通常會將表單資料發送到指定的URL,但我們希望使用AJAX請求來處理表單提交,而不是通過瀏覽器。
我們啟動前端和後端專案
此時我們想要測試前端api有沒有成功呼叫,卻發現瀏覽器跑出以下錯誤訊息:
login:1 Access to XMLHttpRequest at 'http://localhost:5200/api/auth/login' from origin 'http://localhost:3000' has been blocked by
CORS
policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
這是因為CORS
的緣故
什麼是CORS
CORS,全名為跨來源資源共用(Cross-Origin Resource Sharing),是一個網頁瀏覽器的安全性機制,用於管理網頁應用程式在不同來源(域名、協議或端口)之間的資源請求。當一個網頁應用程式試圖在網頁中發出對不同來源的HTTP請求(例如,從域名A的網頁向域名B的伺服器請求資源)時,瀏覽器將根據同源政策(Same-Origin Policy)來阻止這種跨來源的請求。
詳細資訊請見CORS機制
那我們要怎麼解決這問題?
看到上面錯誤訊息裡有一句 No 'Access-Control-Allow-Origin' header is present on the requested resource
,Access-Control-Allow-Origin
就是一種header,所以我們要回到我們後端的專案,在 response 加上它,透過這個方式告訴瀏覽器允許這個 origin 跨來源存取資源。
我們要使用專門處理CORS的套件-cors來解決這個問題。
回到我們的後端專案,安裝套件
npm install cors
到server.js
引入cors並使用
//server.js
const express = require('express');
const cors = require('cors'); //引入套件
const connectDB = require('./config/db');
const app = express();
const bodyParser = require('body-parser');
const users = require('./routes/api/users-route');
const auth = require('./routes/api/auth-route');
const posts = require('./routes/api/posts-route');
const images = require('./routes/api/images-route');
connectDB();
// 啟用 CORS 中介軟體
app.use(cors());
app.use(bodyParser.json());
(略...)
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
使用app.use(cors())
將 CORS 中介軟體添加到 Express 應用程式中。預設情況下,它會允許所有來源的請求
以下是它的預設config
{
"origin": "*", // 設定允許的來源
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE", // 設定允許的 HTTP 方法
"preflightContinue": false, //將 CORS 預檢請求的回應傳遞給下一個handler。
"optionsSuccessStatus": 204 // 設定預檢請求的成功狀態碼
}
如果我們要設置特定的來源才可以存取,可以這樣調整程式:
//server.js
const express = require('express');
const cors = require('cors'); //引入套件
const connectDB = require('./config/db');
const app = express();
const bodyParser = require('body-parser');
const users = require('./routes/api/users-route');
const auth = require('./routes/api/auth-route');
const posts = require('./routes/api/posts-route');
const images = require('./routes/api/images-route');
connectDB();
//cors設置
const corsOptions = {
origin: 'http://localhost:3000', //我們將前端運行的port設定進來
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', // 設定允許的 HTTP 方法
optionsSuccessStatus: 204,
};
// 啟用 CORS 中介軟體
app.use(cors(corsOptions));
app.use(bodyParser.json());
(略...)
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
此時回到前端頁面上,啟動專案就能成功呼叫登入API了
今天快速的處理完表單的驗證和其驗證樣式的顯示,可以看到有些程式邏輯是有點重複的,尤其是表單相關的邏輯,我們於下一篇會透過建立Custom hook
使得表單檢核的程式變得更加乾淨。