Files
cs-note/hzh/TEST/React.md
T
2026-05-24 11:42:38 +08:00

29 KiB
Raw Blame History

tags, create time, update time, status
tags create time update time status
前端
React
Hooks
路由
测试
自我考察
2026-04-30 15:00 2026-04-30 15:45 reviewed

React Hook 与路由状态测试

使用说明

本试卷覆盖 Hooks 核心机制 → 自定义 Hooks → 路由状态管理 → 工程实践 全链路知识点。共四部分:选择题、编程题、场景分析、综合应用。

建议用时: 45 分钟 | 及格线: 70 / 100

做完后对照下方「参考答案」自评,错的地方回到对应笔记重新理解。


一、选择题(每题 3 分,共 45 分)

每题只有一个正确答案。

Q1.【useState 的状态更新机制】

以下关于 useState 的描述,错误的是:

A. 同一批次内连续调用多次 setState 会被合并优化 B. 状态更新是异步的,因此更新后立即访问 state 可能是旧值 C. 当新状态与旧状态使用 Object.is 比较为相同值时,组件不会重新渲染 D. 可以直接修改 state 对象来触发重新渲染

[!tip]- Q1 答案 D — React 要求状态不可变,直接修改 state 对象不会触发重新渲染

解析: React 的状态更新遵循不可变性原则。直接修改 state 对象(如 state.count = count + 1)不会触发组件重新渲染,因为 React 无法检测到这种变化。必须通过 setState 返回新对象。

为什么其他选项正确:

  • ✅ A:React 会将同一批次内的多次 setState 合并为一次更新,这是批处理优化
  • ✅ B:setState 是异步的(在 React 18 中使用自动批处理),更新后立即访问 state 可能仍是旧值
  • ✅ C:React 使用 Object.is 比较新旧状态,如果相同则跳过渲染
// ❌ 错误:直接修改
state.count = state.count + 1;

// ✅ 正确:返回新对象
setCount(prev => prev + 1);

💡 核心原则: 状态不可变性是 React 的基石,它让 React 能够通过浅比较快速判断是否需要重新渲染。

Q2.【useEffect 的依赖数组】

useEffect(() => {
  fetchData();
}, [userId]);

当 userId 变化时,以下哪种说法正确?

A. 先执行清理函数(如果有的话),再执行 effect B. 直接执行 effect,忽略之前的 effect C. 必须在 effect 内部手动调用 cleanup D. 以上都不对

[!tip]- Q2 答案 A — React 在每次执行新的 effect 之前,会先执行上一次 effect 的清理函数

解析: 这是 React 的清理机制核心设计。当依赖项变化时:

  1. 先执行上一次 effect 的清理函数(cleanup)
  2. 再执行新的 effect

这种设计确保了资源的正确释放,避免内存泄漏。

useEffect(() => {
  const controller = new AbortController();
  // effect 逻辑
  return () => controller.abort(); // 清理函数
}, [userId]);

💡 应用场景: 取消未完成的请求、清除定时器、取消事件监听器等。

Q3.【路由参数获取】

在 React Router v6+ 中,以下哪种方式不能获取动态路由参数 /user/:id 中的 id?

A. useParams() hook B. useMatch('/user/:id') 返回的 match 对象 C. useLocation() 解析 pathname D. 直接从 props 读取 props.params.id

[!tip]- Q3 答案 D — 函数组件不再通过 props 接收路由参数

解析: 在 React Router v6+ 中,函数组件使用 hooks 获取路由参数,而不是通过 props。props.params 是 Class 组件时代的 React Router v5 写法。

方式 是否可行 说明
useParams() ✅ 最常用,返回参数对象
useMatch() ✅ 返回匹配信息,包含 params
useLocation() ⚠️ 可以解析 pathname,但需要手动提取
props.params ❌ v5 写法,v6 不支持
// React Router v6+ 正确做法
const { id } = useParams();

💡 迁移注意: 从 v5 迁移到 v6 时,需要将 props.params.xxx 改为 useParams().xxx。

Q4.【自定义 Hook 的调用规则】

以下关于自定义 Hook 的代码,哪个会导致运行时错误?

A. 在组件顶层调用 useCustomHook() B. 在 useEffect 回调中调用 useCustomHook() C. 在条件语句中调用 useCustomHook() D. 在事件处理函数中调用 useCustomHook()

[!tip]- Q4 答案 C — Hooks 必须在组件顶层或自定义 Hook 中调用,不能在条件语句、循环或嵌套函数中调用

解析: 这违反了 Hooks 的调用顺序规则。React 依赖 Hook 的调用顺序来正确关联状态和 Effect。如果在条件语句中调用 Hook,当条件变化时,Hook 的调用顺序也会变化,导致状态错乱。

// ❌ 错误:条件调用
if (isLoggedIn) {
  const data = useUserData(); // 违反规则
}

// ✅ 正确:顶层调用
const data = useUserData();
const result = isLoggedIn ? data : null;

💡 记忆口诀: "Hook 只能在顶层调用,不在条件、循环、嵌套函数中使用"。

Q5.【嵌套路由的 Outlet 使用】

关于 React Router 的 <Outlet> 组件,以下哪项描述正确?

A. Outlet 必须在父路由组件中渲染 B. Outlet 只能渲染一层嵌套 C. Outlet 的位置可以是任意的,只要在父组件中 D. Outlet 渲染的内容由父路由的 element 属性决定

[!tip]- Q5 答案 A — Outlet 必须在父路由组件中渲染

解析: <Outlet> 是嵌套路由的核心,它作为占位符,渲染匹配的子路由组件。每个嵌套路由的父组件都必须包含一个 <Outlet>,否则子路由无法渲染。

// 父路由组件
function Layout() {
  return (
    <div>
      <Sidebar />
      <Outlet />  {/* 子路由在此渲染 */}
    </div>
  );
}

// 路由配置
<Route path="/dashboard" element={<Layout />}>
  <Route path="profile" element={<Profile />} />
  <Route path="settings" element={<Settings />} />
</Route>

为什么其他选项错误:

  • ❌ B:Outlet 可以渲染任意深度的嵌套
  • ❌ C:位置可以任意,但通常放在需要显示子路由内容的地方
  • ❌ D:Outlet 渲染的是匹配的子路由组件,不是父路由的 element

💡 最佳实践: Outlet 通常放在布局组件中,用于渲染子页面内容。


Q6.【Context 的重新渲染行为】

const ThemeContext = createContext({ theme: 'light', setTheme: () => {} });

function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <Sidebar />
    </ThemeContext.Provider>
  );
}

关于以上代码,以下说法正确的是:

A. Header 和 Sidebar 只有在 theme 变化时才会重新渲染 B. Header 和 Sidebar 在任何时候都不会重新渲染,因为它们没有直接订阅 context C. 只要 value 引用变化,所有消费该 context 的组件都会重新渲染,即使用到的值没变 D. 应该在每次渲染时创建新的 context 值,以确保数据最新

[!tip]- Q6 答案 C — Context Provider 使用引用比较来判断是否需要通知消费者

解析: value={{ theme, setTheme }} 在每次渲染时都是新对象,导致所有消费该 context 的组件都重新渲染。即使用了的值没变也会渲染。

解决方案:

// ✅ 使用 useMemo 稳定 value 引用(仅当相关值变化时更新)
const value = useMemo(() => ({ theme, setTheme }), [theme]);

// ✅ 或拆分 context,只让需要的部分变化
const ThemeValueContext = createContext('light');
const SetThemeContext = createContext(() => {});

💡 核心原则: context 是「全有或全无」的通知机制,无法按字段粒度优化。需要细粒度控制时应拆分 context 或使用状态管理库。

为什么其他选项错误:

  • ❌ A:忽略了 value 引用变化的影响
  • ❌ B:消费 context 的组件会在 provider value 变化时重新渲染
  • ❌ D:恰恰相反,应稳定 value 引用以避免不必要的渲染

Q7.【React 虚拟 DOM Diff 算法】

关于 React 的 Diff 算法,以下描述正确的是:

A. React 会对树进行深度优先对比,逐个节点比较 B. React 只对同层组件进行比较,不同层的组件不会跨层对比 C. React 通过 key 属性来精确匹配组件身份,避免不必要的重建 D. 当列表元素的顺序发生变化时,React 会重新排列 DOM 节点而不是重建

[!tip]- Q7 答案 B、C、D 均为正确描述(本题为多选题)

解析: React 的 Diff 算法采用三种启发式策略简化复杂度:

  1. 只对同层比较:不同树的节点直接销毁重建,不会跨层移动
  2. key 匹配身份:列表中使用稳定的 key 帮助 React 识别哪些元素变了
  3. 类型相同则复用:同层同类型的组件会复用实例,仅更新变化的 props
// ❌ 不推荐:用 index 作为 key,会导致不必要的状态重置和渲染错误
{items.map((item, index) => <Item key={index} item={item} />)}

// ✅ 推荐:使用稳定且唯一的 id 作为 key
{items.map(item => <Item key={item.id} item={item} />)}

💡 性能关键: key 的作用是帮助 React 识别「哪个元素变了」而非「是否应该渲染」。没有 key 时 React 依赖索引,列表增删时会出错。

为什么 A 错误: React 的 diff 是同级比较,不是纯粹的深度优先,且采用了类型优先的比较策略。

Q8.【useCallback 与 useMemo 的区别】

以下关于 useCallback 和 useMemo 的说法,错误的是:

A. useCallback(fn, deps) 等价于 useMemo(() => fn, deps) B. 两者返回的值在依赖不变时都是稳定的引用 C. useMemo 用于缓存计算结果,useCallback 用于缓存函数本身 D. 如果没有传入依赖数组,两者的行为与直接使用原始值没有区别

[!tip]- Q8 答案 D — 没有依赖数组时,每次渲染都会重新计算,失去缓存意义

解析: 省略依赖数组意味着 Hooks 不会做任何缓存——每次渲染都会执行回调并返回新值,这与普通变量的行为无异。

对比:

Hook 返回值 典型用途
useCallback(fn, deps) 缓存的函数引用 传给子组件的回调
useMemo(() => val, deps) 缓存的计算结果 昂贵计算、衍生状态
// ✅ 正确:有依赖数组
const memoizedFn = useCallback(() => doSomething(a, b), [a, b]);
const memoizedValue = useMemo(() => expensiveCalc(data), [data]);

// ⚠️ 常见误区:不加 deps 等于白写
const badFn = useCallback(() => doSomething(a), []); // a 变化时不会更新

💡 最佳实践: 只在传参给 memo 子组件或有第三方库依赖时使用 useCallback,日常函数不需要过度优化。

Q9.【Error Boundary 的能力边界】

以下哪种错误不能被 Error Boundary 捕获?

A. 事件处理函数中的错误 B. 生命周期钩子中的错误 C. 组件树渲染过程中的错误 D. useEffect 中的异步错误

[!tip]- Q9 答案 A、D — Error Boundary 只能捕获渲染期、生命周期期和构造期的同步错误

解析: Error Boundary 是一个特殊的 React 组件,通过声明周期方法捕获子组件树的错误。但它有明确的限制:

场景 能否捕获 原因
渲染阶段 ✅ 标准捕获范围
生命周期 ✅ 标准捕获范围
构造函数 ✅ 标准捕获范围
事件处理 ❌ 属于异步回调,不在捕获范围
useEffect 异步 ❌ 异步操作不在捕获范围
自身错误 ❌ 不会捕获自己抛出的错误
路由 ❌ React Router 的错误不属于 React 抛出
异常处理 ❌ try/catch 无法捕获 Promise 未处理错误

正确的错误处理方式:

// 事件处理中自行处理
function handleClick() {
  try {
    riskyOperation();
  } catch (err) {
    handleError(err);
  }
}

// useEffect 中的异步错误
useEffect(() => {
  fetchData().catch(err => console.error(err));
}, []);

// Error Boundary 组件
class ErrorBoundary extends React.Component {
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    reportToService(error, errorInfo);
  }
  render() {
    if (this.state.hasError) return <FallbackUI />;
    return this.props.children;
  }
}

💡 记忆要点: Error Boundary 只管「渲染时」的错误,异步路径一律不管。

Q10.【Portal 的使用场景】

ReactDOM.createPortal(child, container) 的主要作用是什么?

A. 将子节点渲染到父组件 DOM 层级之外的指定容器中 B. 提升子组件的状态到全局 store C. 绕过 React 的生命周期直接进入原生 DOM 操作 D. 将子组件的内容克隆到多个位置同时渲染

[!tip]- Q10 答案 A — Portal 允许将子树渲染到 DOM 树的任何位置

解析: Portal 改变了渲染的 DOM 位置,但保持 React 树结构不变。这意味着事件冒泡仍然按照 React 组件树的层次关系向上冒泡,不受 DOM 结构影响。

// Modal 渲染到 document.body,但在 React 树中仍属于 Dialog 组件
function Modal({ isOpen, children }) {
  if (!isOpen) return null;
  return createPortal(
    <div className="modal-overlay">{children}</div>,
    document.body
  );
}

// 事件冒泡仍然是:Button → Dialog → Modal,不会因为 DOM 分离而中断

常见使用场景:

  • 模态框 / 弹窗(需要突破 z-index 和 overflow:hidden 的限制)
  • Tooltip / 下拉菜单(需要覆盖多个层级的定位)
  • 固定定位的全局组件(Toast、Loading 遮罩等)

💡 注意: Portal 改变的是 DOM 挂载位置,不改变 React 组件层级。父子关系、Context、事件冒泡照常工作。


二、编程题(每题 10 分,共 40 分)

实现题目要求的功能,写出核心代码。

Q11.【自定义 Hook:useFetch】

要求:实现一个通用的 useFetch hook,支持加载状态、错误处理和数据缓存。

// 请补全以下 Hook 的实现
function useFetch<T>(url: string): {
  data: T | null;
  loading: boolean;
  error: Error | null;
} {
  // 你的实现
}

// 使用示例
function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useFetch<User>(`/api/users/${userId}`);

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  return <div>{data?.name}</div>;
}

[!tip]- Q11 参考实现

function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') setError(err);
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

考察要点:

  • useState 多状态管理
  • useEffect 副作用处理
  • AbortController 取消请求
  • 错误边界处理
  • loading 状态管理

💡 进阶优化: 可以添加缓存机制、防抖、重试功能等。

Q12.【路由守卫实现】

要求:实现一个路由守卫组件,根据用户认证状态决定是否允许访问。

// 要求:
// 1. 已登录用户:直接渲染 children
// 2. 未登录用户:重定向到 /login,并保存当前路径以便登录后返回
// 3. 登录后自动跳转回原页面

// 请实现以下组件
function ProtectedRoute({ children }: { children: React.ReactNode }) {
  // 你的实现
}

// 路由配置示例
<Route path="/dashboard" element={
  <ProtectedRoute>
    <Dashboard />
  </ProtectedRoute>
} />

[!tip]- Q12 参考实现

function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const { isAuthenticated } = useAuth();
  const location = useLocation();

  if (!isAuthenticated) {
    // 保存当前路径,登录后可返回
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return <>{children}</>;
}

// 登录页处理
function LoginPage() {
  const navigate = useNavigate();
  const location = useLocation();
  const { login } = useAuth();

  const handleLogin = async (credentials: LoginCredentials) => {
    await login(credentials);
    // 从 state 获取原始路径
    const from = (location.state as any)?.from?.pathname || '/';
    navigate(from, { replace: true });
  };

  // ... 登录表单
}

考察要点:

  • useNavigate 编程式导航
  • useLocation 获取当前路径
  • Navigate 组件的 state 和 replace 属性
  • 登录后返回原页面的实现思路

💡 安全考虑: 生产环境应验证 state.from 的合法性,防止开放重定向漏洞。

Q13.【购物车状态同步】

假设你正在开发一个电商应用,购物车状态需要在以下地方同步:

  1. 商品列表页的"加入购物车"按钮
  2. 购物车页面的商品列表和总价
  3. 导航栏的购物车数量徽章

要求:使用 Context API 实现这个功能,写出核心代码结构。

[!tip]- Q13 参考实现

// CartContext.tsx
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  total: number;
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
}

const CartContext = createContext<CartState | null>(null);

export function CartProvider({ children }: { children: React.ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);

  const addItem = useCallback((product: Omit<CartItem, 'quantity'>) => {
    setItems(prev => {
      const existing = prev.find(item => item.id === product.id);
      if (existing) {
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...prev, { ...product, quantity: 1 }];
    });
  }, []);

  const total = useMemo(
    () => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    [items]
  );

  return (
    <CartContext.Provider value={{ items, total, addItem, ... }}>
      {children}
    </CartContext.Provider>
  );
}

// 自定义 Hook
export function useCart() {
  const context = useContext(CartContext);
  if (!context) throw new Error('useCart must be used within CartProvider');
  return context;
}

考察要点:

  • Context 创建和 Provider
  • 自定义 Hook 封装
  • useCallback 和 useMemo 优化
  • 状态更新逻辑

💡 性能优化: 可以拆分 Context,将频繁变化的数据和不常变化的函数分离。

Q14.【useLocalStorage Hook】

要求:实现一个与 useState API 完全兼容的 useLocalStorage hook,支持自动同步到 localStorage。

// 使用示例
const [theme, setTheme] = useLocalStorage('theme', 'light');
// 等同于 useState,但值会持久化到 localStorage

要求:

  1. 支持懒初始化(与 useState 相同)
  2. 支持函数式更新
  3. 当 key 变化时,从对应 key 读取值
  4. 处理 localStorage 不可用的情况

[!tip]- Q14 参考实现

function useLocalStorage<T>(
  key: string,
  initialValue: T | (() => T)
): [T, React.Dispatch<React.SetStateAction<T>>] {
  // 懒初始化:从 localStorage 读取
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) :
        initialValue instanceof Function ? initialValue() : initialValue;
    } catch {
      return initialValue instanceof Function ? initialValue() : initialValue;
    }
  });

  // 当 key 变化时,重新从 localStorage 读取
  useEffect(() => {
    try {
      const item = window.localStorage.getItem(key);
      if (item) setStoredValue(JSON.parse(item));
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
    }
  }, [key]);

  // 包装 setter,同时更新 localStorage
  const setValue: React.Dispatch<React.SetStateAction<T>> = useCallback((value) => {
    setStoredValue(prev => {
      const nextValue = value instanceof Function ? value(prev) : value;
      try {
        window.localStorage.setItem(key, JSON.stringify(nextValue));
      } catch (error) {
        console.error(`Error setting localStorage key "${key}":`, error);
      }
      return nextValue;
    });
  }, [key]);

  return [storedValue, setValue];
}

考察要点:

  • useState 懒初始化模式
  • useCallback 优化函数引用稳定性
  • useEffect 依赖 key 变化重新同步
  • localStorage 错误处理(容量超限、隐私模式等)

💡 扩展: 可以添加跨标签页同步功能,监听 storage 事件。


三、场景分析(每题 10 分,共 20 分)

分析问题原因并给出解决方案。

Q15.【避免无关组件重新渲染】

当购物车组件频繁更新时,如何避免无关组件重新渲染?列出至少两种优化方案。

[!tip]- Q15 参考答案

方案 1:拆分 Context

// 分离频繁变化的数据和不常变化的函数
const CartItemsContext = createContext<CartItem[]>([]);
const CartActionsContext = createContext<{ addItem: Function }>({ addItem: () => {} });

// 使用时只订阅需要的部分
function CartBadge() {
  const items = useContext(CartItemsContext); // 只在 items 变化时渲染
  return <Badge count={items.length} />;
}

方案 2:使用 useReducer + useMemo

function cartReducer(state: CartItem[], action: CartAction): CartItem[] {
  switch (action.type) {
    case 'ADD': /* ... */
  }
}

function CartProvider({ children }) {
  const [items, dispatch] = useReducer(cartReducer, []);

  const actions = useMemo(() => ({
    addItem: (product) => dispatch({ type: 'ADD', payload: product }),
    // ...
  }), []); // dispatch 稳定,actions 不会变

  return <CartContext.Provider value={{ items, actions }}>{children}</CartContext.Provider>;
}

方案 3:使用 memo 和 useMemo

const CartItem = React.memo(({ item }: { item: CartItem }) => {
  // 只在 item 变化时重新渲染
  return <div>{item.name}</div>;
});

💡 核心原则: 最小化渲染范围,只让真正需要更新的组件重新渲染。

Q16.【路由状态持久化】

问题:用户在 A 页面填写了表单但未提交,跳转到 B 页面后再返回,表单数据丢失。请分析原因并给出解决方案。

// A 页面
function PageA() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  return <form>...</form>;
}

// 路由配置
<Route path="/a" element={<PageA />} />
<Route path="/b" element={<PageB />} />

[!tip]- Q16 参考答案

原因分析:

  • 当路由从 /a 切换到 /b 时,PageA 组件卸载
  • useState 的状态是组件实例的一部分,卸载后状态丢失
  • 返回 /a 时重新挂载 PageA,状态重置为初始值

解决方案 1:状态提升到父组件

function FormContainer() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  return (
    <Routes>
      <Route path="/a" element={<PageA data={formData} onChange={setFormData} />} />
      <Route path="/b" element={<PageB />} />
    </Routes>
  );
}

解决方案 2:URL 搜索参数持久化

function PageA() {
  const [searchParams, setSearchParams] = useSearchParams();
  const name = searchParams.get('name') || '';

  const updateName = (value: string) => {
    setSearchParams(prev => {
      prev.set('name', value);
      return prev;
    });
  };
}

解决方案 3:使用 sessionStorage

function usePersistedState<T>(key: string, initialValue: T) {
  const [state, setState] = useState<T>(() => {
    const saved = sessionStorage.getItem(key);
    return saved ? JSON.parse(saved) : initialValue;
  });

  useEffect(() => {
    sessionStorage.setItem(key, JSON.stringify(state));
  }, [key, state]);

  return [state, setState];
}

方案对比:

方案 优点 缺点 适用场景
状态提升 简单直接 父组件会重新渲染 少量表单
URL 参数 可分享、可书签 有长度限制 简单表单
sessionStorage 自动同步 需要手动清理 复杂表单

💡 推荐: 简单表单用 URL 参数,复杂表单用 sessionStorage 或全局状态管理。


四、综合应用(每题 10 分,共 10 分)

综合运用所学知识解决复杂问题。

Q17.【完整的路由认证系统】

设计一个完整的路由认证系统,包括:

  1. 登录页面
  2. 受保护的路由
  3. 角色权限控制(admin/user)
  4. 自动登过期处理

要求:画出架构图并写出核心代码。

[!tip]- Q17 参考答案

架构图:

graph TD
    A["用户访问"] --> B{"是否已登录?"}
    B -->|否| C["重定向到 /login"]
    B -->|是| D{"检查 Token 过期"}
    D -->|过期| E["自动刷新 Token"]
    D -->|有效| F{"检查角色权限"}
    F -->|权限不足| G["重定向到 /403"]
    F -->|权限足够| H["渲染页面"]
    E -->|刷新成功| D
    E -->|刷新失败| C
    C --> I["登录页面"]
    I -->|登录成功| J["保存 Token"]
    J --> H
    ```

**核心代码:**

```tsx
// AuthContext.tsx
interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthState | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [token, setToken] = useState<string | null>(null);

  // Token 过期检查
  useEffect(() => {
    if (!token) return;
    const expiration = decodeToken(token).exp * 1000;
    const timeout = expiration - Date.now() - 60000; // 提前 1 分钟刷新

    const timer = setTimeout(async () => {
      try {
        const newToken = await refreshToken(token);
        setToken(newToken);
      } catch {
        logout();
      }
    }, timeout);

    return () => clearTimeout(timer);
  }, [token]);

  const value = {
    user,
    token,
    isAuthenticated: !!token,
    login: async (credentials) => {
      const { user, token } = await authService.login(credentials);
      setUser(user);
      setToken(token);
    },
    logout: () => {
      setUser(null);
      setToken(null);
    },
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// ProtectedRoute.tsx
function ProtectedRoute({
  children,
  requiredRole,
}: {
  children: React.ReactNode;
  requiredRole?: 'admin' | 'user';
}) {
  const { isAuthenticated, user } = useAuth();
  const location = useLocation();

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (requiredRole && user?.role !== requiredRole) {
    return <Navigate to="/403" replace />;
  }

  return <>{children}</>;
}

// 路由配置
<Routes>
  <Route path="/login" element={<LoginPage />} />
  <Route
    path="/admin"
    element={
      <ProtectedRoute requiredRole="admin">
        <AdminDashboard />
      </ProtectedRoute>
    }
  />
  <Route
    path="/user"
    element={
      <ProtectedRoute>
        <UserDashboard />
      </ProtectedRoute>
    }
  />
</Routes>

考察要点:

  • Context 状态管理
  • Token 过期处理
  • 角色权限控制
  • 路由守卫实现
  • 登录后跳转

💡 安全建议:

  • Token 存储在 httpOnly cookie 中比 localStorage 更安全
  • 定期刷新 Token,避免长时间使用同一个 Token
  • 服务端也要验证权限,不能只依赖前端

评分参考

题目类型 题目 满分 权重
选择题 Q1-Q10 45 分 45%
编程题 Q11-Q14 40 分 40%
场景分析 Q15-Q16 20 分 20%
综合应用 Q17 10 分 10%
总计 100 分 100%

及格线: ≥70 分
优秀线: ≥85 分