核心状态管理(Context + localStorage)
首先,我们创建一个认证上下文(AuthContext),它负责管理用户的登录状态,并自动将状态持久化到 localStorage。
// AuthContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
// 1. 创建 Context 对象
const AuthContext = createContext();
// 2. 创建 Context Provider 组件
export const AuthProvider = ({ children }) => {
// 尝试从 localStorage 读取初始状态
const [isLoggedIn, setIsLoggedIn] = useState(() => {
const saved = localStorage.getItem('isLoggedIn');
return saved === 'true'; // 将字符串转换为布尔值
});
const [userToken, setUserToken] = useState(() => {
return localStorage.getItem('userToken') || null;
});
// 登录函数
const login = (token) => {
setIsLoggedIn(true);
setUserToken(token);
// 将状态持久化到 localStorage
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('userToken', token);
};
// 登出函数
const logout = () => {
setIsLoggedIn(false);
setUserToken(null);
// 从 localStorage 清除状态
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('userToken');
};
// 提供 Context 值
const value = {
isLoggedIn,
userToken,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
// 3. 创建自定义 Hook,方便使用 Context
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth 必须在 AuthProvider 内使用');
}
return context;
};
export default AuthContext;
🛡️ 路由守卫高阶组件 (HOC)
接下来,我们创建一个高阶组件,用于保护那些需要登录才能访问的页面。
// withAuth.jsx (高阶组件)
import { useAuth } from './AuthContext';
import { Navigate, useLocation } from 'react-router-dom';
const withAuth = (WrappedComponent) => {
return function AuthComponent(props) {
const { isLoggedIn } = useAuth();
const location = useLocation();
// 如果未登录,重定向到登录页,并记录当前想访问的页面
if (!isLoggedIn) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
// 如果已登录,渲染目标组件
return <WrappedComponent {...props} />;
};
};
export default withAuth;
🔗 应用入口与路由配置
然后,我们在应用的根组件中设置路由,并使用 AuthProvider包裹整个应用。
// App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './AuthContext';
import withAuth from './withAuth';
// 页面组件
import HomePage from './pages/HomePage';
import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/DashboardPage'; // 需要登录的页面
import ProfilePage from './pages/ProfilePage'; // 需要登录的页面
// 使用高阶组件包装需要认证的页面
const AuthenticatedDashboard = withAuth(DashboardPage);
const AuthenticatedProfile = withAuth(ProfilePage);
function App() {
return (
<AuthProvider>
<Router>
<Routes>
{/* 公开路由 */}
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
{/* 受保护的路由 */}
<Route path="/dashboard" element={<AuthenticatedDashboard />} />
<Route path="/profile" element={<AuthenticatedProfile />} />
</Routes>
</Router>
</AuthProvider>
);
}
export default App;
📝 登录页面实现
登录页面需要调用 AuthContext中的 login方法来更新全局状态。
// LoginPage.jsx
import React, { useState } from 'react';
import { useAuth } from './AuthContext';
import { useNavigate, useLocation } from 'react-router-dom';
function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
// 获取重定向路径,如果没有则默认跳转到首页
const from = location.state?.from?.pathname || '/';
const handleSubmit = async (e) => {
e.preventDefault();
// 这里是模拟登录API调用
try {
// 实际项目中,这里应该是调用真实的登录接口
const response = await fakeLoginAPI(username, password);
if (response.success) {
// 调用 Context 中的 login 方法更新状态
login(response.token);
// 登录成功后跳转
navigate(from, { replace: true });
} else {
alert('登录失败: ' + response.message);
}
} catch (error) {
alert('登录出错: ' + error.message);
}
};
// 模拟登录API
const fakeLoginAPI = (username, password) => {
return new Promise((resolve) => {
setTimeout(() => {
if (username === 'admin' && password === 'password') {
resolve({
success: true,
token: 'fake-jwt-token-' + Date.now(),
user: { id: 1, username: 'admin' }
});
} else {
resolve({
success: false,
message: '用户名或密码错误'
});
}
}, 1000);
});
};
return (
<div style={{ padding: '20px' }}>
<h2>登录页面</h2>
<form onSubmit={handleSubmit}>
<div>
<label>用户名: </label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label>密码: </label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">登录</button>
</form>
<p>提示: 试用用户名 admin 密码 password</p>
</div>
);
}
export default LoginPage;
💡 在普通组件中使用认证状态
在任何组件中,你都可以使用 useAuthHook 来获取认证状态和用户信息。
// UserProfile.jsx
import { useAuth } from './AuthContext';
function UserProfile() {
const { isLoggedIn, userToken, logout } = useAuth();
return (
<div>
{isLoggedIn ? (
<div>
<p>当前已登录</p>
<p>Token: {userToken ? `${userToken.substring(0, 15)}...` : '无'}</p>
<button onClick={logout}>退出登录</button>
</div>
) : (
<p>请先登录</p>
)}
</div>
);
}
⚠️ 关键实现细节说明
这个完整方案的核心优势在于:
- 状态持久化:通过
localStorage,用户刷新页面后登录状态依然保持。 - 全局状态管理:使用
Context使得登录状态可以在组件树的任何地方被访问和修改。 - 路由保护:高阶组件
withAuth优雅地实现了路由拦截功能。 - 用户体验:登录后能自动跳转回用户原本想访问的页面。