--- tags: - 前端 - React - Hooks - 路由 - 测试 - 自我考察 create time: 2026-04-30 15:00 update time: 2026-04-30 15:45 status: 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` 比较新旧状态,如果相同则跳过渲染 > > ```tsx > // ❌ 错误:直接修改 > state.count = state.count + 1; > > // ✅ 正确:返回新对象 > setCount(prev => prev + 1); > ``` > > 💡 **核心原则:** 状态不可变性是 React 的基石,它让 React 能够通过浅比较快速判断是否需要重新渲染。 ### Q2.【useEffect 的依赖数组】 ```tsx 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 > > 这种设计确保了资源的正确释放,避免内存泄漏。 > > ```tsx > 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 不支持 | > > ```tsx > // 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 的调用顺序也会变化,导致状态错乱。 > > ```tsx > // ❌ 错误:条件调用 > if (isLoggedIn) { > const data = useUserData(); // 违反规则 > } > > // ✅ 正确:顶层调用 > const data = useUserData(); > const result = isLoggedIn ? data : null; > ``` > > 💡 **记忆口诀:** "Hook 只能在顶层调用,不在条件、循环、嵌套函数中使用"。 ### Q5.【嵌套路由的 Outlet 使用】 关于 React Router 的 `` 组件,以下哪项描述正确? A. Outlet 必须在父路由组件中渲染 B. Outlet 只能渲染一层嵌套 C. Outlet 的位置可以是任意的,只要在父组件中 D. Outlet 渲染的内容由父路由的 `element` 属性决定 > [!tip]- Q5 答案 > **A — Outlet 必须在父路由组件中渲染** > > **解析:** > `` 是嵌套路由的核心,它作为占位符,渲染匹配的子路由组件。每个嵌套路由的父组件都必须包含一个 ``,否则子路由无法渲染。 > > ```tsx > // 父路由组件 > function Layout() { > return ( >
> > {/* 子路由在此渲染 */} >
> ); > } > > // 路由配置 > }> > } /> > } /> > > ``` > > **为什么其他选项错误:** > - ❌ B:Outlet 可以渲染任意深度的嵌套 > - ❌ C:位置可以任意,但通常放在需要显示子路由内容的地方 > - ❌ D:Outlet 渲染的是匹配的子路由组件,不是父路由的 element > > 💡 **最佳实践:** Outlet 通常放在布局组件中,用于渲染子页面内容。 --- ### Q6.【Context 的重新渲染行为】 ```tsx const ThemeContext = createContext({ theme: 'light', setTheme: () => {} }); function App() { const [theme, setTheme] = useState('light'); return (
); } ``` 关于以上代码,以下说法**正确**的是: A. `Header` 和 `Sidebar` 只有在 `theme` 变化时才会重新渲染 B. `Header` 和 `Sidebar` 在任何时候都不会重新渲染,因为它们没有直接订阅 context C. 只要 `value` 引用变化,所有消费该 context 的组件都会重新渲染,即使用到的值没变 D. 应该在每次渲染时创建新的 context 值,以确保数据最新 > [!tip]- Q6 答案 > **C — Context Provider 使用引用比较来判断是否需要通知消费者** > > **解析:** > `value={{ theme, setTheme }}` 在每次渲染时都是新对象,导致所有消费该 context 的组件都重新渲染。即使用了的值没变也会渲染。 > > **解决方案:** > ```tsx > // ✅ 使用 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 > > ```tsx > // ❌ 不推荐:用 index 作为 key,会导致不必要的状态重置和渲染错误 > {items.map((item, index) => )} > > // ✅ 推荐:使用稳定且唯一的 id 作为 key > {items.map(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)` | 缓存的计算结果 | 昂贵计算、衍生状态 | > > ```tsx > // ✅ 正确:有依赖数组 > 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 未处理错误 | > > **正确的错误处理方式:** > ```tsx > // 事件处理中自行处理 > 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 ; > 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 结构影响。 > > ```tsx > // Modal 渲染到 document.body,但在 React 树中仍属于 Dialog 组件 > function Modal({ isOpen, children }) { > if (!isOpen) return null; > return createPortal( >
{children}
, > document.body > ); > } > > // 事件冒泡仍然是:Button → Dialog → Modal,不会因为 DOM 分离而中断 > ``` > > **常见使用场景:** > - 模态框 / 弹窗(需要突破 z-index 和 overflow:hidden 的限制) > - Tooltip / 下拉菜单(需要覆盖多个层级的定位) > - 固定定位的全局组件(Toast、Loading 遮罩等) > > 💡 **注意:** Portal 改变的是 DOM 挂载位置,不改变 React 组件层级。父子关系、Context、事件冒泡照常工作。 --- ## 二、编程题(每题 10 分,共 40 分) > 实现题目要求的功能,写出核心代码。 ### Q11.【自定义 Hook:useFetch】 **要求**:实现一个通用的 `useFetch` hook,支持加载状态、错误处理和数据缓存。 ```tsx // 请补全以下 Hook 的实现 function useFetch(url: string): { data: T | null; loading: boolean; error: Error | null; } { // 你的实现 } // 使用示例 function UserProfile({ userId }: { userId: string }) { const { data, loading, error } = useFetch(`/api/users/${userId}`); if (loading) return ; if (error) return ; return
{data?.name}
; } ``` > [!tip]- Q11 参考实现 > > ```tsx > function useFetch(url: string) { > const [data, setData] = useState(null); > const [loading, setLoading] = useState(true); > const [error, setError] = useState(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.【路由守卫实现】 **要求**:实现一个路由守卫组件,根据用户认证状态决定是否允许访问。 ```tsx // 要求: // 1. 已登录用户:直接渲染 children // 2. 未登录用户:重定向到 /login,并保存当前路径以便登录后返回 // 3. 登录后自动跳转回原页面 // 请实现以下组件 function ProtectedRoute({ children }: { children: React.ReactNode }) { // 你的实现 } // 路由配置示例 } /> ``` > [!tip]- Q12 参考实现 > > ```tsx > function ProtectedRoute({ children }: { children: React.ReactNode }) { > const { isAuthenticated } = useAuth(); > const location = useLocation(); > > if (!isAuthenticated) { > // 保存当前路径,登录后可返回 > return ; > } > > 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 参考实现 > > ```tsx > // CartContext.tsx > interface CartItem { > id: string; > name: string; > price: number; > quantity: number; > } > > interface CartState { > items: CartItem[]; > total: number; > addItem: (item: Omit) => void; > removeItem: (id: string) => void; > updateQuantity: (id: string, quantity: number) => void; > } > > const CartContext = createContext(null); > > export function CartProvider({ children }: { children: React.ReactNode }) { > const [items, setItems] = useState([]); > > const addItem = useCallback((product: Omit) => { > 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 ( > > {children} > > ); > } > > // 自定义 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。 ```tsx // 使用示例 const [theme, setTheme] = useLocalStorage('theme', 'light'); // 等同于 useState,但值会持久化到 localStorage ``` **要求**: 1. 支持懒初始化(与 useState 相同) 2. 支持函数式更新 3. 当 key 变化时,从对应 key 读取值 4. 处理 localStorage 不可用的情况 > [!tip]- Q14 参考实现 > > ```tsx > function useLocalStorage( > key: string, > initialValue: T | (() => T) > ): [T, React.Dispatch>] { > // 懒初始化:从 localStorage 读取 > const [storedValue, setStoredValue] = useState(() => { > 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> = 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** > > ```tsx > // 分离频繁变化的数据和不常变化的函数 > const CartItemsContext = createContext([]); > const CartActionsContext = createContext<{ addItem: Function }>({ addItem: () => {} }); > > // 使用时只订阅需要的部分 > function CartBadge() { > const items = useContext(CartItemsContext); // 只在 items 变化时渲染 > return ; > } > ``` > > **方案 2:使用 useReducer + useMemo** > > ```tsx > 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 {children}; > } > ``` > > **方案 3:使用 memo 和 useMemo** > > ```tsx > const CartItem = React.memo(({ item }: { item: CartItem }) => { > // 只在 item 变化时重新渲染 > return
{item.name}
; > }); > ``` > > 💡 **核心原则:** 最小化渲染范围,只让真正需要更新的组件重新渲染。 ### Q16.【路由状态持久化】 **问题**:用户在 A 页面填写了表单但未提交,跳转到 B 页面后再返回,表单数据丢失。请分析原因并给出解决方案。 ```tsx // A 页面 function PageA() { const [formData, setFormData] = useState({ name: '', email: '' }); return
...
; } // 路由配置 } /> } /> ``` > [!tip]- Q16 参考答案 > > **原因分析:** > - 当路由从 `/a` 切换到 `/b` 时,`PageA` 组件卸载 > - `useState` 的状态是组件实例的一部分,卸载后状态丢失 > - 返回 `/a` 时重新挂载 `PageA`,状态重置为初始值 > > **解决方案 1:状态提升到父组件** > > ```tsx > function FormContainer() { > const [formData, setFormData] = useState({ name: '', email: '' }); > return ( > > } /> > } /> > > ); > } > ``` > > **解决方案 2:URL 搜索参数持久化** > > ```tsx > 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** > > ```tsx > function usePersistedState(key: string, initialValue: T) { > const [state, setState] = useState(() => { > 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 参考答案 > > **架构图:** > > ```mermaid > 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; > logout: () => void; > } > > const AuthContext = createContext(null); > > export function AuthProvider({ children }: { children: React.ReactNode }) { > const [user, setUser] = useState(null); > const [token, setToken] = useState(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 {children}; > } > > // ProtectedRoute.tsx > function ProtectedRoute({ > children, > requiredRole, > }: { > children: React.ReactNode; > requiredRole?: 'admin' | 'user'; > }) { > const { isAuthenticated, user } = useAuth(); > const location = useLocation(); > > if (!isAuthenticated) { > return ; > } > > if (requiredRole && user?.role !== requiredRole) { > return ; > } > > return <>{children}; > } > > // 路由配置 > > } /> > path="/admin" > element={ > > > > } > /> > path="/user" > element={ > > > > } > /> > > ``` > > **考察要点:** > - 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 分