iT邦幫忙

2023 iThome 鐵人賽

DAY 20
0

大綱

  1. Wireframe
  2. 登入頁開發
  3. CORS處理

1. Wireframe

以下是登入和註冊頁面的wireframe,樣式細節可依個人喜好調整

登入頁

https://ithelp.ithome.com.tw/upload/images/20231005/20136558RGcfygOzfT.jpg

註冊頁

https://ithelp.ithome.com.tw/upload/images/20231005/20136558k6b2rgKd8c.jpg

2. 登入頁開發

可以看到上面兩個圖的外層樣式是一樣的,只有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
https://ithelp.ithome.com.tw/upload/images/20231005/20136558I1dU0HI7TL.jpg

//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;

🔺補充資訊:
handleSubmitevent.preventDefault()是用來阻止表單的預設提交行為。因為當我們在HTML表單中按下提交按鈕時,瀏覽器通常會將表單資料發送到指定的URL,但我們希望使用AJAX請求來處理表單提交,而不是通過瀏覽器。


登入功能測試

我們啟動前端後端專案

https://ithelp.ithome.com.tw/upload/images/20231005/20136558v8i9d90LlA.jpg

此時我們想要測試前端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.
/images/emoticon/emoticon04.gif

這是因為CORS的緣故

3. 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 resourceAccess-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了
https://ithelp.ithome.com.tw/upload/images/20231005/201365581pgLJlo4fG.jpg

結語

今天快速的處理完表單的驗證和其驗證樣式的顯示,可以看到有些程式邏輯是有點重複的,尤其是表單相關的邏輯,我們於下一篇會透過建立Custom hook使得表單檢核的程式變得更加乾淨。

參考資源


上一篇
[Day19]React pages 和 routes設定
下一篇
[Day21] 使用Custom Hooks改寫表單驗證、註冊頁開發
系列文
初探全端之旅: 以MERN技術建立個人部落格31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言