用 React 制作登录页面:从表单到受保护的页面

用 React 制作登录页面:从表单到受保护的页面

登录页面几乎是每个网站都需要的部分。本文用 React 一步步制作:表单结构、受控输入、表单校验、模拟登录、多次失败后锁定、用 Context 共享用户状态,以及让未登录的人看不到内容的受保护页面。每个示例都已实际运行,显示的输出是真实结果。

需要准备的东西

需要 Node.js 18 或更高版本以及 React 基础。示例使用 React 18.3.1,并用本文的测试运行器运行,该运行器通过 jsdom 在模拟浏览器中点击按钮。注意:在浏览器中检查密码仅用于学习。真实系统必须在服务器上验证密码,以哈希形式保存,并使用 HTTPS。

示意图

登录流程:输入、校验、发送到服务器验证、保存状态、保护页面
登录流程:输入、校验、发送到服务器验证、保存状态、保护页面

示例 1: 登录表单结构

从包含邮箱输入框、密码输入框和提交按钮的表单开始。renderToStaticMarkup 显示 React 生成的 HTML。

import React from "react";

export function App() {
  return (
    <form>
      <h2>เข้าสู่ระบบ</h2>
      <label>อีเมล<input type="email" name="email" /></label>
      <label>รหัสผ่าน<input type="password" name="password" /></label>
      <button type="submit">เข้าสู่ระบบ</button>
    </form>
  );
}

实际输出

<form><h2>เข้าสู่ระบบ</h2><label>อีเมล<input type="email" name="email"/></label><label>รหัสผ่าน<input type="password" name="password"/></label><button type="submit">เข้าสู่ระบบ</button></form>

示例 2: 显示或隐藏密码

将可见状态保存在 state 中,点击按钮时在 password 与 text 之间切换输入框类型。

1. 点击一次显示密码按钮(从关到开)

import React, { useState } from "react";

export function App() {
  const [show, setShow] = useState(false);
  return (
    <div>
      <input type={show ? "text" : "password"} value="secret1" readOnly />
      <button id="toggle" onClick={() => setShow(!show)}>แสดงรหัสผ่าน</button>
      <p data-out>แสดงรหัสผ่าน: {show ? "เปิด" : "ปิด"}</p>
    </div>
  );
}

export const steps = ["#toggle"];

实际输出

ข้อความเริ่มต้น: แสดงรหัสผ่าน: ปิด
หลังคลิกครั้งที่ 1: แสดงรหัสผ่าน: เปิด

示例 3: 校验输入内容

validate 函数返回错误信息列表。只在用户提交后显示,避免用户还在输入时就提示错误。

import React, { useState } from "react";

// กติกาการตรวจสอบ: คืนรายการข้อความแจ้งเตือน ถ้าไม่มีข้อผิดพลาดจะได้อาร์เรย์ว่าง
export function validate(email, password) {
  const errors = [];
  if (!email) errors.push("กรอกอีเมล");
  else if (!/^\S+@\S+\.\S+$/.test(email)) errors.push("รูปแบบอีเมลไม่ถูกต้อง");
  if (password.length < 6) errors.push("รหัสผ่านอย่างน้อย 6 ตัว");
  return errors;
}

export function App() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [submitted, setSubmitted] = useState(false);
  const errors = validate(email, password);
  return (
    <div>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input value={password} onChange={e => setPassword(e.target.value)} />
      <button id="submit" onClick={() => setSubmitted(true)}>ส่งฟอร์ม</button>
      <p data-out>{!submitted ? "ยังไม่ส่งฟอร์ม" : errors.length ? errors.join(" | ") : "ไม่มีข้อผิดพลาด"}</p>
    </div>
  );
}

export const steps = ["#submit"];

实际输出

ข้อความเริ่มต้น: ยังไม่ส่งฟอร์ม
หลังคลิกครั้งที่ 1: กรอกอีเมล | รหัสผ่านอย่างน้อย 6 ตัว

示例 4: 模拟登录

将邮箱和密码与示例数据比较,然后显示成功或账号不存在的消息。本例从正确的值开始。

import React, { useState } from "react";

// ตัวอย่างฐานข้อมูลผู้ใช้จำลอง จริง ๆ ต้องตรวจที่เซิร์ฟเวอร์เท่านั้น
const USERS = { "[email protected]": "secret1" };

export function login(email, password) {
  if (USERS[email] === undefined) return { ok: false, message: "ไม่พบบัญชีนี้" };
  if (USERS[email] !== password) return { ok: false, message: "รหัสผ่านไม่ถูกต้อง" };
  return { ok: true, message: "เข้าสู่ระบบสำเร็จ: " + email };
}

export function App() {
  const [email, setEmail] = useState("[email protected]");
  const [password, setPassword] = useState("secret1");
  const [result, setResult] = useState("ยังไม่ได้ลองเข้าสู่ระบบ");
  return (
    <div>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input value={password} onChange={e => setPassword(e.target.value)} />
      <button id="submit" onClick={() => setResult(login(email, password).message)}>เข้าสู่ระบบ</button>
      <p data-out>{result}</p>
    </div>
  );
}

export const steps = ["#submit"];

实际输出

ข้อความเริ่มต้น: ยังไม่ได้ลองเข้าสู่ระบบ
หลังคลิกครั้งที่ 1: เข้าสู่ระบบสำเร็จ: [email protected]

示例 5: 多次失败后锁定

统计密码错误次数,达到上限时禁用按钮,以减缓重复猜测密码。

import React, { useState } from "react";

const MAX_TRIES = 3;

export function App() {
  const [password, setPassword] = useState("wrong");
  const [tries, setTries] = useState(0);
  const [message, setMessage] = useState("");
  const locked = tries >= MAX_TRIES;

  function submit() {
    if (locked) return;
    if (password === "secret1") {
      setMessage("เข้าสู่ระบบสำเร็จ");
      return;
    }
    const next = tries + 1;
    setTries(next);
    setMessage(next >= MAX_TRIES ? "ล็อกชั่วคราว ลองใหม่ภายหลัง" : `รหัสผ่านไม่ถูกต้อง (ครั้งที่ ${next} จาก ${MAX_TRIES})`);
  }

  return (
    <div>
      <input value={password} onChange={e => setPassword(e.target.value)} />
      <button id="submit" disabled={locked} onClick={submit}>เข้าสู่ระบบ</button>
      <p data-out>{message || "พร้อมใช้งาน"}</p>
    </div>
  );
}

export const steps = ["#submit", "#submit", "#submit"];

实际输出

ข้อความเริ่มต้น: พร้อมใช้งาน
หลังคลิกครั้งที่ 1: รหัสผ่านไม่ถูกต้อง (ครั้งที่ 1 จาก 3)
หลังคลิกครั้งที่ 2: รหัสผ่านไม่ถูกต้อง (ครั้งที่ 2 จาก 3)
หลังคลิกครั้งที่ 3: ล็อกชั่วคราว ลองใหม่ภายหลัง

示例 6: 记住登录选项

由 state 控制的复选框,并立即向用户显示状态。

import React, { useState } from "react";

export function App() {
  const [remember, setRemember] = useState(false);
  return (
    <div>
      <label>
        <input id="remember" type="checkbox" checked={remember} onChange={e => setRemember(e.target.checked)} />
        จดจำการเข้าสู่ระบบ
      </label>
      <p data-out>จดจำการเข้าสู่ระบบ: {remember ? "เปิด" : "ปิด"}</p>
    </div>
  );
}

export const steps = ["#remember"];

实际输出

ข้อความเริ่มต้น: จดจำการเข้าสู่ระบบ: ปิด
หลังคลิกครั้งที่ 1: จดจำการเข้าสู่ระบบ: เปิด

示例 7: 用 Context 保存用户状态

创建 AuthContext,使状态栏和按钮无需逐层传递 props 即可访问用户数据。

import React, { createContext, useContext, useState } from "react";

// เก็บสถานะผู้ใช้ไว้ที่ Context เพื่อให้ทุกส่วนของหน้าเข้าถึงได้โดยไม่ต้องส่ง props ต่อกันหลายชั้น
const AuthContext = createContext(null);

function StatusBar() {
  const { user, login, logout } = useContext(AuthContext);
  return (
    <div>
      <p data-out>สถานะ: {user ? "สวัสดี " + user : "ยังไม่ได้เข้าสู่ระบบ"}</p>
      {user
        ? <button id="logout" onClick={logout}>ออกจากระบบ</button>
        : <button id="login" onClick={() => login("สมชาย")}>เข้าสู่ระบบ</button>}
    </div>
  );
}

export function App() {
  const [user, setUser] = useState(null);
  const value = { user, login: setUser, logout: () => setUser(null) };
  return (
    <AuthContext.Provider value={value}>
      <StatusBar />
    </AuthContext.Provider>
  );
}

export const steps = ["#login", "#logout"];

实际输出

ข้อความเริ่มต้น: สถานะ: ยังไม่ได้เข้าสู่ระบบ
หลังคลิกครั้งที่ 1: สถานะ: สวัสดี สมชาย
หลังคลิกครั้งที่ 2: สถานะ: ยังไม่ได้เข้าสู่ระบบ

示例 8: 需要登录的页面

没有用户时提示登录,有用户时显示仪表盘。这就是受保护路由的思路。

1. 先登录,再退出登录

import React, { useState } from "react";

// หน้าที่ต้องเข้าสู่ระบบก่อนจึงจะเห็น ถ้ายังไม่มีผู้ใช้จะแสดงข้อความให้เข้าสู่ระบบแทน
function Dashboard({ user }) {
  if (!user) return <p data-out>กรุณาเข้าสู่ระบบก่อน</p>;
  return <p data-out>หน้าแดชบอร์ดของ {user}</p>;
}

export function App() {
  const [user, setUser] = useState(null);
  return (
    <div>
      <button id="login" onClick={() => setUser("สมชาย")}>เข้าสู่ระบบ</button>
      <button id="logout" onClick={() => setUser(null)}>ออกจากระบบ</button>
      <Dashboard user={user} />
    </div>
  );
}

export const steps = ["#login", "#logout"];

实际输出

ข้อความเริ่มต้น: กรุณาเข้าสู่ระบบก่อน
หลังคลิกครั้งที่ 1: หน้าแดชบอร์ดของ สมชาย
หลังคลิกครั้งที่ 2: กรุณาเข้าสู่ระบบก่อน

示例 9: 完整的登录页面

最终页面组合了表单、带正确 autocomplete 提示的输入框、记住登录选项以及忘记密码链接。

import React from "react";

// หน้าเข้าสู่ระบบแบบสมบูรณ์ที่รวมทุกส่วนไว้ด้วยกัน: ฟอร์ม ช่องกรอก ตัวเลือกจดจำ และลิงก์ลืมรหัสผ่าน
export function App() {
  return (
    <main>
      <h1>เข้าสู่ระบบ</h1>
      <form>
        <label>อีเมล<input type="email" name="email" autoComplete="username" /></label>
        <label>รหัสผ่าน<input type="password" name="password" autoComplete="current-password" /></label>
        <label><input type="checkbox" name="remember" /> จดจำการเข้าสู่ระบบ</label>
        <button type="submit">เข้าสู่ระบบ</button>
      </form>
      <a href="/forgot">ลืมรหัสผ่าน?</a>
    </main>
  );
}

实际输出

<main><h1>เข้าสู่ระบบ</h1><form><label>อีเมล<input type="email" name="email" autoComplete="username"/></label><label>รหัสผ่าน<input type="password" name="password" autoComplete="current-password"/></label><label><input type="checkbox" name="remember"/> จดจำการเข้าสู่ระบบ</label><button type="submit">เข้าสู่ระบบ</button></form><a href="/forgot">ลืมรหัสผ่าน?</a></main>

下一步

下一步可以用 fetch 将登录页面连接到真实 API,使用 HTTP-only Cookie 或安全令牌,并添加通过邮件发送重置链接的忘记密码功能。

总结

好的登录页面由受 state 控制的表单、校验、失败次数限制、通过 Context 共享的用户状态,以及在显示数据前检查登录状态的页面组成。真实系统必须始终在服务器上检查访问权限。