React 中实现登录状态管理的完整方案

85 阅读3分钟

核心状态管理(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>
  );
}

⚠️ 关键实现细节说明

这个完整方案的核心优势在于:

  1. ​状态持久化​​:通过 localStorage,用户刷新页面后登录状态依然保持。
  2. ​全局状态管理​​:使用 Context使得登录状态可以在组件树的任何地方被访问和修改。
  3. ​路由保护​​:高阶组件 withAuth优雅地实现了路由拦截功能。
  4. ​用户体验​​:登录后能自动跳转回用户原本想访问的页面。