基于一个完整的 JWT 登录鉴权 Demo,带你系统掌握 React 核心概念与工程实践
前言
React 是当前最流行的前端框架之一,但很多初学者在学习过程中会遇到这样的问题:
- 看了很多教程,但不知道如何组织一个完整的项目
- 理解了组件、状态等概念,但不知道如何应用到实际场景
- 知道 React Router、Zustand、Axios,但不知道如何协同工作
本文将通过一个完整的 JWT 登录鉴权项目,带你从零到一构建一个 React 应用。你将学到:
- React 基础:组件、JSX、Props、状态管理
- 路由系统:React Router、懒加载、路由守卫
- 状态管理:useState、useEffect、Zustand 全局状态
- API 层:Axios 封装、拦截器、JWT 鉴权
- 工程实践:项目结构、代码组织、最佳实践
一、项目架构总览
1.1 目录结构
bash
login-demo/
├── mock/ # Mock 服务(模拟后端)
│ └── user.js # JWT 签发与验证
├── src/
│ ├── api/ # API 层
│ │ ├── config.js # Axios 实例 + 拦截器
│ │ ├── user.js # 登录 API
│ │ └── repo.js # 受保护资源 API
│ ├── components/ # 公共组件
│ │ ├── Nav.jsx # 导航栏
│ │ └── RequireAuth.jsx # 路由守卫
│ ├── pages/ # 页面组件
│ │ ├── Home.jsx # 首页
│ │ ├── Login.jsx # 登录页
│ │ └── Pay.jsx # 支付页(受保护)
│ ├── store/ # 全局状态
│ │ └── user.js # 用户状态(Zustand)
│ ├── App.jsx # 路由配置
│ └── main.jsx # 入口文件
└── vite.config.js # Vite 配置 + Mock 插件
1.2 技术栈
- React 19 - UI 框架
- React Router DOM - 路由管理
- Zustand - 轻量级状态管理
- Axios - HTTP 请求
- JWT - 登录鉴权
- Vite - 构建工具
- vite-plugin-mock - Mock 服务
1.3 完整流程图
javascript
用户登录 → Mock 签发 JWT → 存储到 localStorage + Zustand
↓
后续请求 → Axios 拦截器自动加 Authorization: Bearer xxx
↓
访问受保护页面 → RequireAuth 检查 token → 有则放行,无则重定向
↓
请求受保护资源 → Mock 验证 JWT → 返回数据
二、React 基础:组件与 JSX
2.1 什么是组件?
React 应用由组件组成。组件是独立的、可复用的 UI 单元。
jsx
// 一个简单的组件
function Home() {
return <div>Home 首页</div>;
}
关键点:
- 组件名必须大写开头(React 通过大小写区分组件和 HTML 标签)
- 组件返回 JSX(JavaScript XML,类似 HTML 的语法)
- 组件可以接收 props(父组件传入的数据)
2.2 JSX 语法
JSX 是 React 的核心语法,它允许你在 JavaScript 中写类似 HTML 的代码。
jsx
function Nav() {
const token = 'xxx'; // 假设有 token
return (
<nav>
{/* 条件渲染 */}
{!token && <a href="/login">Login</a>}
{/* 变量插值 */}
{token && <span>已登录</span>}
{/* 事件绑定 */}
<button onClick={() => console.log('clicked')}>点击</button>
</nav>
);
}
关键点:
- JSX 中用
{}包裹 JavaScript 表达式 - 支持条件渲染(
&&、三元运算符) - 支持列表渲染(
map) - 事件名用驼峰命名(
onClick而非onclick)
2.3 Props:组件间通信
Props 是父组件向子组件传递数据的方式。
jsx
// 父组件
function App() {
return <Pay user="admin" amount={100} />;
}
// 子组件
function Pay({ user, amount }) {
return (
<div>
<p>用户: {user}</p>
<p>金额: ¥{amount}</p>
</div>
);
}
关键点:
- Props 是只读的,子组件不能修改
- 可以传递任意类型的数据(字符串、数字、对象、函数、组件)
- 通过解构赋值简化 props 使用
三、状态管理:useState 与 useEffect
3.1 useState:组件内部状态
useState 是 React 最常用的 Hook,用于管理组件内部状态。
jsx
import { useState } from 'react';
function Pay() {
const [amount, setAmount] = useState('');
const [paid, setPaid] = useState(false);
const handlePay = (e) => {
e.preventDefault();
if (amount && Number(amount) > 0) {
setPaid(true);
}
};
return (
<form onSubmit={handlePay}>
<input
type="number"
value={amount}
onChange={e => setAmount(e.target.value)}
/>
<button type="submit">支付</button>
</form>
);
}
关键点:
useState返回一个数组:[当前值, 更新函数]- 更新状态会触发组件重新渲染
- 状态更新是异步的,不能依赖当前值计算下一个值
jsx
// ❌ 错误:依赖当前值
setCount(count + 1);
setCount(count + 1); // 可能只加 1
// ✅ 正确:使用函数式更新
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 会加 2
3.2 useEffect:副作用处理
useEffect 用于处理副作用(数据请求、订阅、DOM 操作等)。
jsx
import { useState, useEffect } from 'react';
function Home() {
const [repo, setRepo] = useState(null);
const token = useAuthStore(state => state.token);
// 依赖 token,token 变化时重新执行
useEffect(() => {
if (!token) return; // 无 token 不请求
(async () => {
const res = await getRepo();
if (res.code === 0) {
setRepo(res.data);
}
})();
}, [token]); // 依赖数组:token 变化时执行
return <div>{repo && <pre>{JSON.stringify(repo)}</pre>}</div>;
}
关键点:
useEffect在组件渲染后执行- 第二个参数是依赖数组 ,决定何时重新执行
[]:只在组件挂载时执行一次[token]:token 变化时执行- 不传:每次渲染都执行(慎用)
- 可以返回一个清理函数,在组件卸载或下次执行前调用
jsx
useEffect(() => {
const timer = setInterval(() => {
console.log('每秒执行');
}, 1000);
// 清理函数:组件卸载时清除定时器
return () => clearInterval(timer);
}, []);
3.3 表单处理:受控组件
React 中表单元素的状态由组件控制,称为受控组件。
jsx
function Login() {
const [formData, setFormData] = useState({ username: '', password: '' });
const handleChange = e => {
const { name, value } = e.target;
// 使用计算属性名更新对应字段
setFormData(prev => ({ ...prev, [name]: value }));
};
return (
<form>
<input
name="username"
value={formData.username}
onChange={handleChange}
/>
<input
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
</form>
);
}
关键点:
- 表单元素的
value由 React 状态控制 onChange事件更新状态- 使用
name属性统一处理多个字段
四、路由系统:React Router
4.1 基础路由配置
React Router 是 React 的标准路由库。
jsx
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const Login = lazy(() => import('./pages/Login'));
const Pay = lazy(() => import('./pages/Pay'));
function App() {
return (
<Router>
<Suspense fallback={<div>loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/pay" element={<Pay />} />
</Routes>
</Suspense>
</Router>
);
}
关键点:
Router:路由容器,提供路由上下文Routes:路由集合Route:单个路由规则,path匹配 URL,element渲染组件lazy:懒加载组件,按需加载,提升性能Suspense:懒加载时的占位 UI
4.2 路由导航
使用 useNavigate 进行编程式导航。
jsx
import { useNavigate, useLocation } from 'react-router-dom';
function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from || '/'; // 获取来源路径
const handleLogin = async () => {
// ... 登录逻辑
navigate(from, { replace: true }); // 登录成功后跳转
};
return <button onClick={handleLogin}>登录</button>;
}
关键点:
useNavigate:返回导航函数useLocation:返回当前路由信息location.state:可以传递状态数据replace: true:替换当前历史记录(用户不能返回)
4.3 路由守卫:保护页面
路由守卫用于保护需要登录才能访问的页面。
jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../store/user';
function RequireAuth({ children }) {
const token = useAuthStore(state => state.token);
const location = useLocation();
if (!token) {
// 未登录,重定向到登录页,并保存来源路径
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
return children; // 已登录,渲染子组件
}
// 使用
<Route path="/pay" element={
<RequireAuth>
<Pay />
</RequireAuth>
} />
关键点:
RequireAuth是一个高阶组件,包裹需要保护的页面- 检查 token,无 token 则重定向
- 通过
state传递来源路径,登录后跳回
五、状态管理:Zustand 全局状态
5.1 为什么需要全局状态?
组件内部状态(useState)只能在组件内部使用。但有些状态需要跨组件共享,比如:
- 用户登录信息(token、user)
- 主题设置(深色/浅色)
- 购物车数据
传统方案是 Context API ,但代码较繁琐。Zustand 是一个轻量级替代方案。
5.2 Zustand 基础用法
jsx
import { create } from 'zustand';
// 创建 store
export const useAuthStore = create(set => ({
// 状态
token: localStorage.getItem('token') || '',
user: JSON.parse(localStorage.getItem('user')) || null,
// 动作(修改状态的方法)
setAuth: ({ token, user }) => {
localStorage.setItem('token', token);
localStorage.setItem('user', JSON.stringify(user));
set({ token, user }); // 更新状态
},
logout: () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
set({ token: '', user: null });
}
}));
5.3 在组件中使用
jsx
function Nav() {
// 选择性订阅状态
const token = useAuthStore(state => state.token);
const user = useAuthStore(state => state.user);
const logout = useAuthStore(state => state.logout);
return (
<nav>
{user && <span>{user.username}</span>}
{token && <button onClick={logout}>退出</button>}
</nav>
);
}
function Login() {
const setAuth = useAuthStore(state => state.setAuth);
const handleLogin = async () => {
const res = await login(formData);
if (res.code === 0) {
setAuth({ token: res.token, user: res.user }); // 更新全局状态
}
};
}
关键点:
create创建 store,返回一个 Hook- 状态和动作在同一个对象中定义
- 组件通过 Hook 订阅状态,只有订阅的状态变化才会重新渲染
- 可以持久化到
localStorage,刷新后恢复
5.4 Zustand vs Context
| 特性 | Zustand | Context |
|---|---|---|
| 代码量 | 少 | 多 |
| 性能 | 自动优化 | 需要手动优化 |
| 学习曲线 | 低 | 中 |
| 适用场景 | 中小型项目 | 大型项目 |
六、API 层:Axios 封装
6.1 创建 Axios 实例
jsx
import axios from 'axios';
const instance = axios.create({
baseURL: '/api', // 基础 URL
timeout: 5000 // 超时时间
});
关键点:
axios.create创建独立实例,避免全局配置污染baseURL:所有请求会自动拼接timeout:请求超时时间
6.2 请求拦截器:自动加 Token
jsx
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
关键点:
- 拦截器在每个请求发出前执行
- 自动从
localStorage读取 token - 添加到请求头的
Authorization字段
6.3 响应拦截器:统一处理
jsx
instance.interceptors.response.use(res => {
return res.data; // 直接返回数据,无需 res.data
});
关键点:
- 响应拦截器在收到响应后执行
- 统一返回
res.data,简化调用方代码
6.4 封装 API 方法
jsx
// api/user.js
import axios from './config';
export const login = async (data) => {
const res = await axios.post('/login', data);
return res;
};
// api/repo.js
import axios from './config';
export const getRepo = async () => {
const res = await axios.get('/repo');
return res;
};
关键点:
- 每个 API 方法独立文件
- 统一使用封装的 axios 实例
- 返回 Promise,调用方使用
await
七、JWT 鉴权:完整流程
7.1 什么是 JWT?
JWT(JSON Web Token)是一种用于身份验证的开放标准。它由三部分组成:
- Header:算法和类型
- Payload:用户数据(JSON)
- Signature:签名(防篡改)
7.2 登录流程
markdown
1. 用户提交表单(username + password)
↓
2. 调用 /api/login
↓
3. Mock 验证账号密码
↓
4. 签发 JWT:jwt.sign({ user, role }, secret)
↓
5. 返回 token 给前端
↓
6. 前端存储到 localStorage + Zustand
7.3 请求受保护资源
markdown
1. 前端请求 /api/repo
↓
2. Axios 拦截器自动加 Authorization: Bearer xxx
↓
3. Mock 从请求头提取 token
↓
4. 验证 token:jwt.verify(token, secret)
↓
5. 返回用户数据
7.4 Mock 实现
jsx
import jwt from 'jsonwebtoken';
const secret = 'secret819!$';
export default [
{
url: '/api/login',
method: 'post',
response: (req) => {
const { username, password } = req.body;
if (username !== 'admin' || password !== '123456') {
return { code: -1, message: '账号或密码错误' };
}
// 签发 JWT
const token = jwt.sign(
{ user: username, role: 'admin' },
secret,
{ expiresIn: 86400 } // 24小时过期
);
return {
code: 0,
user: { username },
token
};
}
},
{
url: '/api/repo',
method: 'get',
response: (req) => {
try {
// 从请求头提取 token
const token = req.headers['authorization'].split(' ')[1];
// 验证 token
const decoded = jwt.verify(token, secret);
return {
code: 0,
data: decoded.user
};
} catch {
return {
code: 401,
msg: 'Invalid token'
};
}
}
}
];
八、最佳实践
8.1 项目结构
bash
src/
├── api/ # API 层(与后端交互)
├── components/ # 公共组件(跨页面复用)
├── pages/ # 页面组件(对应路由)
├── store/ # 全局状态
├── utils/ # 工具函数
└── App.jsx # 路由配置
关键点:
- 按功能模块划分目录
- 组件命名用 PascalCase
- 文件命名用 kebab-case 或 PascalCase
8.2 组件设计原则
- 单一职责:一个组件只做一件事
- 可复用性:通过 Props 使组件可复用
- 状态提升:多个组件共享的状态提升到父组件
- 全局状态:跨多个组件共享的状态使用 Zustand
8.3 性能优化
- 懒加载 :使用
lazy+Suspense按需加载组件 - 选择性订阅:Zustand 只订阅需要的状态
- 避免不必要的渲染 :使用
React.memo包裹纯展示组件
jsx
import { memo } from 'react';
const Nav = memo(function Nav() {
// 只有 props 或订阅的状态变化才会重新渲染
return <nav>...</nav>;
});
8.4 错误处理
jsx
const handleLogin = async () => {
try {
const res = await login(formData);
if (res.code === 0) {
setAuth({ token: res.token, user: res.user });
navigate(from, { replace: true });
} else {
alert(res.message || '登录失败');
}
} catch (err) {
console.error(err);
alert('登录失败');
}
};
关键点:
- 使用
try-catch捕获异步错误 - 检查响应码,区分业务错误和系统错误
- 给用户友好的错误提示
九、总结
通过这个项目,你学到了:
- React 基础:组件、JSX、Props、状态管理
- 路由系统:React Router、懒加载、路由守卫
- 状态管理:useState、useEffect、Zustand
- API 层:Axios 封装、拦截器、JWT 鉴权
- 工程实践:项目结构、代码组织、最佳实践
核心概念回顾
- 组件:React 应用的基本单元
- 状态:驱动 UI 更新的数据
- 路由:管理页面导航
- 拦截器:统一处理请求/响应
- JWT:无状态的身份验证方案
下一步
- 学习 TypeScript + React
- 学习服务端渲染(Next.js)
- 学习单元测试(Jest + React Testing Library)
- 学习状态管理进阶(Redux Toolkit)
附录:运行项目
bash
cd login-demo
pnpm install
pnpm dev
测试账号:admin / 123456
访问:
/- 首页(登录后显示受保护资源)/login- 登录页/pay- 支付页(需要登录)
作者 :基于实际项目整理
日期:2026-09-10