新增


鉴权路由
- http 无状态的
- 有状态?
- 请求头 token Authorization
- Cookie 小饼干
- localStorage 本地存储 login 状态 user admin password 123456
- 组件内部的子组件 props.children 拿到组件申明的内部所有的子节点。 model 弹窗组件 mask 蒙层 窗体 头部, 尾部 主题部分children传入。 定制性
{children 定制}
路由对象
- SPA 需要前端路由
- url改变 , 对应不同的资源 resetful 设计理念 hash #/pay browserRouter History
- navigator 导航栏
- location 地址栏
- history 历史对象
- Link 组件 to replace
路由两种选型
- hashRouter url 局部改变 hash 部分 url 有点为了前端路由 , url 有点丑, 后端路由不太一样 /pay #/pay
- browserRouter 不是hash 方案实现SPA
4.9 路由重定向的三大实战场景(金字塔塔尖------核心结论)
路由重定向的本质是"URL 的永久/临时迁移"------当某个 URL 不再指向有效内容时,通过 <Navigate> 组件或 useNavigate Hook 将用户无缝引导到正确的目标页面。它不是简单的"跳转",而是一个包含历史记录管理、状态传递、用户体验考量在内的完整设计模式。
在真实业务中,路由重定向主要有三大场景:活动下线迁移 (营销活动结束后跳转到结果页)、路径重构 (旧 URL 永久迁移到新 URL)、条件重定向(未登录用户访问受保护页面时跳转到登录页)。下面逐一拆解。
4.9.1 场景一:营销活动下线 ------ /game → /result
这是最经典的重定向场景。假设运营团队搞了一个拉新活动,URL 是 /#/game,投入了 100 万推广预算。活动结束后,大量用户仍会通过之前保存的书签、分享链接、短信链接访问 /game。如果直接返回 404,这 100 万的推广效果就打了折扣。
正确的做法是将 /game 重定向到 /result(活动结果页),告诉用户"活动已结束,这是最终中奖名单"。
jsx
// App.jsx 中的配置
<Route path="/game" element={
<Navigate replace to="/result" />
} />
为什么必须使用 replace?
bash
用户从短信点击链接进入 /game:
不使用 replace(push 模式):
历史栈:[短信链接] → [/game] → [/result]
用户点后退 → 回到 /game → 立即又被重定向到 /result
→ 用户被困在死循环中,永远退不回短信来源页面
使用 replace:
历史栈:[短信链接] → [/result]
用户点后退 → 回到短信来源页面 ✔
进一步优化:带参数的重定向
jsx
// 更完善的写法:携带活动 ID,方便数据统计
<Route path="/game/:activityId" element={
<Navigate replace to="/result" />
} />
// 在 Result 页面中,还可以通过更高级的方式获取原活动 ID
// 例如在 Navigate 中携带 state:
<Route path="/game/:activityId" element={
<GameRedirect />
} />
// GameRedirect 组件内部:
function GameRedirect() {
const { activityId } = useParams();
return <Navigate replace to="/result" state={{ fromActivity: activityId }} />;
}
4.9.2 场景二:首页路径归一化 ------ /home → /
很多网站在演进过程中,首页路径可能变更多次:/home、/index、/main 等。为了保证 SEO 和用户体验的一致性,应该将所有旧首页路径重定向到统一的 /。
jsx
// App.jsx 中的配置
<Route path="/home" element={
<Navigate replace to="/" />
} />
<Route path="/index" element={
<Navigate replace to="/" />
} />
深层原理 :搜索引擎爬虫会认为 /home 和 / 是两个不同的页面,导致内容重复惩罚。通过 301(服务端)或客户端重定向,告知搜索引擎"这些 URL 都指向同一个页面"。虽然 HashRouter 的 # 后面内容不发往服务器(搜索引擎通常不索引 hash 路由的路径),但在 BrowserRouter 模式下这至关重要。
4.9.3 场景三:条件重定向 ------ 未登录用户访问 /user/:id → /login
这是最复杂但最常见的场景。用户在未登录状态下访问需要登录才能看的页面(如 /user/123 个人中心),应当被引导到登录页,登录成功后自动返回之前想访问的页面。
bash
用户访问 /user/123(未登录)
│
▼
检测到未登录 → 重定向到 /login,并记住"用户想来 /user/123"
│
▼
用户在 /login 输入用户名密码 → 登录成功
│
▼
自动跳回 /user/123(而非首页!这才是符合用户预期的体验)
实现这个闭环需要三个组件配合 :ProtectRoute(路由守卫)、Login(登录页)、Navigate(重定向指令)。其中的核心机制是 location.state 传递来源信息------这将在下面的 4.10 和 4.11 节详细展开。
4.10 路由守卫(ProtectRoute):权限控制的"门禁系统"
4.10.1 核心结论(金字塔塔尖)
路由守卫是一个包裹在目标页面外层的"安检组件"------它不修改 URL 结构,而是在渲染目标页面之前插入一个鉴权判断:已登录则放行(渲染 children),未登录则拦截并重定向到登录页,同时通过 location.state 记住"用户从哪来",为登录后的回跳埋下伏笔。
ini
用户访问 /pay(受保护页面)
│
▼
ProtectRoute 组件渲染
│
├── isLogin === true → 渲染 {children}(即 <Pay />)
│ 用户正常使用支付功能
│
└── isLogin === false → 返回 <Navigate to="/login" state={{ from: location }} />
用户被送到登录页,location 信息被保存在 state 中
4.10.2 完整源码及逐行解析
jsx
// src/ProtectRoute.jsx(共 27 行)
import {
Navigate, // 重定向组件
useLocation // 获取当前路由位置信息
} from 'react-router-dom';
const ProtectRoute = ({ children }) => {
console.log(children, '-----');
// ↑ children 是 <ProtectRoute> 标签内部包裹的 JSX 元素
// 在 App.jsx 中:<ProtectRoute><Pay /></ProtectRoute>
// children 就是 <Pay /> 这个 React 元素
const location = useLocation();
// ↑ useLocation() 返回当前路由的完整位置对象:
// {
// pathname: "/pay", ← 当前路径
// search: "", ← 查询字符串
// hash: "", ← hash 中的子 hash
// state: null, ← 路由状态(通过 navigate 或 Link 的 state 传递)
// key: "default" ← 唯一标识
// }
// html5 本地存储 ------ 域名的沙盒
// localStorage 是浏览器提供的键值存储,数据在同一个域名下共享
// 每个域名有独立的 localStorage 沙盒(约 5MB),不同域名互不可见
const isLogin = localStorage.getItem('isLogin') === 'true';
console.log(isLogin, 'isLogin');
if (!isLogin) {
// 未登录 → 重定向到登录页
// ★★★ 关键设计:state={{ from: location }} ★★★
// 将当前 location 对象(包含 pathname="/pay")作为 state 传递
// 登录成功后,Login 组件可以从 state 中读取 from,实现回跳
return <Navigate to="/login" replace state={{ from: location }} />;
}
return (
<>
ProtectRoute:
{children} {/* 已登录,放行!渲染被保护的页面组件 */}
</>
);
};
export default ProtectRoute;
4.10.3 children 模式的设计智慧
ProtectRoute 使用了 React 的 children prop 模式,而非直接在路由配置中写判断逻辑:
jsx
// ✔ 当前设计(children 模式)------ ProtectRoute 是通用组件
<Route path="/pay" element={
<ProtectRoute>
<Pay />
</ProtectRoute>
} />
// 可以轻松复用到任何需要保护的页面:
<Route path="/settings" element={
<ProtectRoute>
<Settings />
</ProtectRoute>
} />
<Route path="/orders" element={
<ProtectRoute>
<Orders />
</ProtectRoute>
} />
为什么这比"在每个页面组件里写鉴权逻辑"更好?
javascript
❌ 在每个页面组件中写鉴权(代码重复、容易遗漏):
function Pay() {
const isLogin = localStorage.getItem('isLogin') === 'true';
if (!isLogin) return <Navigate to="/login" />;
// ... 业务逻辑
}
function Settings() {
const isLogin = localStorage.getItem('isLogin') === 'true';
if (!isLogin) return <Navigate to="/login" />;
// ... 业务逻辑
}
// 每个页面都要写一遍鉴权,万一忘记写了就是安全漏洞
✔ 使用 ProtectRoute 包裹(关注点分离、不会遗漏):
// 鉴权逻辑集中在 ProtectRoute 一个组件中
// 页面组件只关心自己的业务逻辑
// 新增受保护页面时,只需要在路由配置中包裹一层即可
4.10.4 state={{ from: location }} 的深度解析
这是整个登录回跳机制最精妙的一行代码:
jsx
return <Navigate to="/login" replace state={{ from: location }} />;
state 是什么?
state 是 React Router 在 URL 之外传递数据的"隐式通道"。它不显示在 URL 中,不会出现在浏览器地址栏,也不会被发送到服务器。它通过浏览器的 History API 的 history.state 机制存储,仅在客户端可见。
为什么要传整个 location 对象而不是只传 pathname?
jsx
// ❌ 只传 pathname ------ 丢失了查询参数和 hash
state={{ from: location.pathname }} // 只拿到 "/pay",丢失了 ?coupon=123
// ✔ 传整个 location ------ 保留了完整信息
state={{ from: location }} // 拿到 { pathname: "/pay", search: "?coupon=123", ... }
真实场景举例:用户通过一个带优惠券参数的链接访问支付页 /pay?coupon=SUMMER50,被拦截到登录页。登录成功后,应当回到 /pay?coupon=SUMMER50 而不是 /pay。传整个 location 对象保证了查询参数不丢失。
replace 的作用再次体现:
在 ProtectRoute 中使用 replace,意味着"用 /login 替换掉 /pay 在历史栈中的位置":
bash
用户直接访问 /pay(未登录):
历史栈:[/pay] → ProtectRoute 执行 replace → [/login]
用户登录成功后 navigate('/pay'):
历史栈:[/pay]
此时用户点后退 → 回到登录前所在页面(可能是首页)
而不是回到 /login(再次被 ProtectRoute 拦截 → 又送回 /login → 死循环)
4.11 登录认证与回跳机制:从拦截到放行的完整闭环
4.11.1 核心结论(金字塔塔尖)
登录不是孤立的页面跳转,而是一个"拦截 → 暂存意图 → 认证 → 恢复意图"的完整状态机。location.state 是这个状态机的"记忆载体"------ProtectRoute 在拦截时通过它记住"用户想去哪",Login 组件在登录成功后通过它恢复"用户原本的目的地"。
php
完整闭环(总览):
用户访问 /pay(未登录)
│
▼
ProtectRoute 检测 !isLogin
│
├── 保存 location = { pathname: "/pay", search: "" } 到 state.from
└── <Navigate replace to="/login" state={{ from: location }} />
│
▼
Login 组件渲染
│
├── useLocation().state?.from?.pathname → "/pay"
│ (从 state 中读出用户原本想去哪)
│
├── 用户输入用户名密码 → 验证 → localStorage.setItem('isLogin', 'true')
│
└── navigate("/pay", { replace: true })
│
▼
URL 变为 /pay → Routes 重新匹配 → ProtectRoute 再次执行
│
├── isLogin === true → 放行!
└── 渲染 <Pay />
4.11.2 Login 组件完整源码及逐行解析
jsx
// src/pages/Login/index.jsx(共 57 行)
import {
useNavigate, // 命令式导航 Hook
useLocation, // 读取当前路由位置(含 state)
} from 'react-router-dom';
const Login = () => {
const navigate = useNavigate();
const location = useLocation();
// ★★★ 核心:从 state 中提取"用户原本想去哪" ★★★
// location.state 可能有三种情况:
// 1. 从 ProtectRoute 拦截来的:location.state = { from: { pathname: "/pay", ... } }
// 2. 从 Link state 传来的:location.state = { from: { pathname: "/user/123", ... } }
// 3. 直接访问 /login:location.state = null(无 state)
//
// 可选链操作符 ?. 是 ES11 语法:
// location.state?.from?.pathname 等价于:
// location.state && location.state.from && location.state.from.pathname
// 如果链上任何一环是 null/undefined,整个表达式返回 undefined(不会报错)
const from = location.state?.from?.pathname || '/';
// 默认回首页 ↑
// 如果用户直接访问 /login(没有来源信息),登录后回首页
console.log(from, 'from');
function handleSubmit(e) {
e.preventDefault(); // 阻止表单的默认提交行为(页面刷新)
// 原生表单数据对象 ------ 不需要 useState 管理每个字段
const formData = new FormData(e.currentTarget);
// FormData 是 HTML5 标准 API,e.currentTarget 指向 <form> DOM 元素
const username = formData.get("username");
const password = formData.get("password");
if (!username || !password) {
alert("请输入用户名和密码");
return;
}
// 硬编码的账号密码验证(生产环境应调用后端 API)
if (username === "admin" && password === "123456") {
// ★ 写入登录状态到 localStorage
localStorage.setItem('isLogin', 'true');
// ★★★ 关键:replace: true ★★★
// 将目标页面的历史记录替换掉 /login 的记录
//
// 为什么要 replace?
// 如果不用 replace,历史栈变成:[/pay拦截前] → [/login] → [/pay]
// 用户在 /pay 点后退 → 回到 /login → 发现已登录 → 又自动跳到 /pay → 死循环
//
// 使用 replace 后,历史栈变成:[/pay拦截前] → [/pay]
// 用户在 /pay 点后退 → 回到拦截前的页面 ✔
navigate(from, { replace: true });
} else {
alert("用户名或密码错误");
}
}
return (
<form onSubmit={handleSubmit}>
<h1>登录</h1>
<input
name="username"
placeholder="请输入用户名"
required
/>
<input
name="password"
placeholder="请输入密码"
type="password"
required
/>
<button type="submit">登录</button>
<p>这是登录页面</p>
</form>
);
};
export default Login;
4.11.3 from 变量的三种取值场景
javascript
const from = location.state?.from?.pathname || '/';
| 用户操作 | location.state |
from 的值 |
登录后跳转 |
|---|---|---|---|
访问 /pay 被 ProtectRoute 拦截 |
{ from: { pathname: "/pay", ... } } |
"/pay" |
回到支付页 |
访问 /user/123 被条件重定向拦截 |
{ from: { pathname: "/user/123", ... } } |
"/user/123" |
回到用户详情页 |
直接访问 /login(比如点了导航栏的"登录") |
null 或 undefined |
"/"(默认值) |
回到首页 |
4.11.4 为什么用 FormData 而不是受控组件?
jsx
// ✔ 当前写法:非受控(uncontrolled)------ 用 FormData 原生 API
function handleSubmit(e) {
const formData = new FormData(e.currentTarget);
const username = formData.get("username");
}
// ❌ 另一种写法:受控(controlled)------ 每个字段一个 useState
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
// 2 个字段需要 2 个 state + 2 个 onChange,10 个字段呢?
选择 FormData 的理由:
- 登录表单字段少但不需要实时验证(不像搜索框需要防抖),FormData 足够
- 避免不必要的重渲染(每次按键都更新 state → 整个组件重渲染)
- 代码更简洁,字段越多优势越明显
4.11.5 replace: true 在登录场景的再次强调
这是登录流程中最容易被忽略的细节,但直接影响用户体验:
bash
login 登录后跳转:
不使用 replace(push 模式):
历史栈:[首页] → [/pay被拦截] → [/login] → [/pay]
↑ 这条被替换掉了(ProtectRoute 的 replace)
↑ 这条是 push 进来的(navigate 默认 push)
用户在 /pay 点后退:
第一次后退 → /login → isLogin=true → 检测到已登录 → 自动跳回 /pay
第二次后退 → /login → 又跳回 /pay
第三次后退 → ... ← 用户被困!永远退不回去!
使用 replace:
历史栈:[首页] → [/pay]
用户在 /pay 点后退:
直接回到首页 ✔ 用户体验完美
4.11.6 localStorage 作为登录状态存储的考量
javascript
// 写入
localStorage.setItem('isLogin', 'true');
// 读取
const isLogin = localStorage.getItem('isLogin') === 'true';
// 清除(退出登录)
localStorage.removeItem('isLogin');
为什么用 localStorage 而不是 sessionStorage?
| 存储方式 | 生命周期 | 适用场景 |
|---|---|---|
localStorage |
永久(除非手动清除) | "记住我"------关闭浏览器后再次打开仍保持登录 |
sessionStorage |
关闭标签页即清除 | 敏感操作------关闭标签页自动退出 |
生产环境中的重要警告 :本项目使用 localStorage 存储登录状态仅用于教学演示。生产环境中绝不应仅依赖 localStorage 做权限控制,因为:
- 用户可以手动修改
localStorage(在 DevTools 中执行localStorage.setItem('isLogin', 'true')即可绕过) localStorage不随 HTTP 请求自动发送,后端无法验证- 真正的权限控制必须由后端通过 token(JWT)/ session 验证,前端路由守卫只是"第一道防线"(提升用户体验),真正的安全在后端
4.12 支付页面(Pay):受保护资源的完整示例
4.12.1 核心结论(金字塔塔尖)
Pay 组件是路由守卫的"被保护对象"------它不包含任何鉴权逻辑,只专注于业务功能(选择支付方式、模拟支付、退出登录)。它的安全性完全由外层的 <ProtectRoute> 保证。这种"关注点分离"的设计,使得业务组件和权限逻辑各自独立、可测试、可复用。
4.12.2 完整源码及关键逻辑
jsx
// src/pages/Pay/index.jsx(共 113 行)
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
const Pay = () => {
const navigate = useNavigate();
const [payMethod, setPayMethod] = useState('wechat'); // 支付方式
const [paying, setPaying] = useState(false); // 支付中状态
const [paySuccess, setPaySuccess] = useState(false); // 支付成功状态
// 模拟订单数据
const order = {
id: 'ORD' + Date.now(),
name: '天龙八部 RAG 助手 - 年度会员',
price: 99.00,
};
// 模拟支付流程
const handlePay = () => {
setPaying(true); // 进入支付中状态 → 按钮变灰、显示"支付中..."
setTimeout(() => {
setPaying(false); // 支付完成
setPaySuccess(true); // 切换到成功页
setTimeout(() => {
navigate('/', { replace: true }); // 2 秒后自动跳回首页
}, 2000);
}, 1500); // 模拟 1.5 秒支付处理
};
// 退出登录
const handleLogout = () => {
localStorage.removeItem('isLogin'); // 清除登录标记
navigate('/login'); // 跳转到登录页
};
// 支付成功后的 UI
if (paySuccess) {
return (
<div style={{ textAlign: 'center', padding: 40 }}>
<h2>支付成功</h2>
<p>订单号:{order.id}</p>
<p>金额:¥{order.price}</p>
<p>即将跳转首页...</p>
</div>
);
}
// 正常支付 UI
return (
<div style={{ padding: 20, maxWidth: 480, margin: '0 auto' }}>
<h2>支付页面</h2>
{/* 订单信息 */}
<div style={{ border: '1px solid #ddd', padding: 16, borderRadius: 8, marginBottom: 16 }}>
<h3>{order.name}</h3>
<p>订单号:{order.id}</p>
<p style={{ fontSize: 24, color: '#e4393c', fontWeight: 'bold' }}>
¥{order.price.toFixed(2)}
</p>
</div>
{/* 支付方式选择 */}
<div style={{ marginBottom: 16 }}>
<h3>选择支付方式</h3>
<label><input type="radio" name="payMethod" value="wechat"
checked={payMethod === 'wechat'}
onChange={(e) => setPayMethod(e.target.value)} /> 微信支付</label>
<label><input type="radio" name="payMethod" value="alipay"
checked={payMethod === 'alipay'}
onChange={(e) => setPayMethod(e.target.value)} /> 支付宝</label>
</div>
{/* 支付按钮 */}
<button onClick={handlePay} disabled={paying}
style={{ backgroundColor: paying ? '#ccc' : '#e4393c', /* ... */ }}>
{paying ? '支付中...' : `确认支付 ¥${order.price.toFixed(2)}`}
</button>
{/* 退出登录按钮 */}
<button onClick={handleLogout} style={{ /* ... */ }}>
退出登录
</button>
</div>
);
};
export default Pay;
4.12.3 组件状态机设计
Pay 组件内部有三个状态,构成了一个简单的状态机:
javascript
初始状态(paying=false, paySuccess=false)
│
├── 用户点击"确认支付"
│ │
│ ▼
│ 支付中(paying=true, paySuccess=false)
│ UI:按钮变灰显示"支付中...",不可再次点击(disabled)
│ │
│ │ setTimeout 1500ms
│ │
│ ▼
│ 支付成功(paying=false, paySuccess=true)
│ UI:显示成功信息 + "即将跳转首页..."
│ │
│ │ setTimeout 2000ms
│ │
│ ▼
│ navigate('/') → 回到首页
├── 用户点击"退出登录"
│ │
│ ▼
│ localStorage.removeItem('isLogin')
│ navigate('/login')
│ │
│ ▼
│ ProtectRoute 检测 !isLogin → 再次拦截 → 送回登录页
4.12.4 退出登录后的路由行为
javascript
const handleLogout = () => {
localStorage.removeItem('isLogin');
navigate('/login');
};
退出登录后导航到 /login,由于 localStorage 中的 isLogin 已被清除,Login 页面正常显示登录表单。此时:
- 如果用户重新登录 →
from为默认值/→ 跳回首页 - 如果用户直接关闭标签页 → session 级别无影响(若用 sessionStorage 则自动清除)
4.13 完整业务流程串联:从支付意图到支付成功的全链路追踪
4.13.1 核心结论(金字塔塔尖)
前端路由的权限控制不是一个孤立的功能点,而是 ProtectRoute(门禁)、Login(认证)、Pay(业务)、Navigate(调度)四个组件协同完成的完整闭环。理解这个闭环的关键在于追踪 location.state 和 localStorage 这两个"隐式数据通道"在各个环节的流转。
4.13.2 时间线追踪
ini
T=0ms 用户状态:未登录,localStorage 中 isLogin 不存在
用户在浏览器输入 URL:http://localhost:5173/#/pay
T=1ms HashRouter 初始化,pathname = "/pay"
T=2ms Routes 开始匹配:
遍历 Route:
path="/" → 不匹配
path="/about" → 不匹配
path="/user/:id" → 不匹配
path="/products" → 不匹配
path="/old-path" → 不匹配
path="/login" → 不匹配
path="/pay" → ✔ 匹配!
渲染 element={<ProtectRoute><Pay /></ProtectRoute>}
T=3ms ProtectRoute 组件执行:
useLocation() → { pathname: "/pay", search: "", state: null, ... }
localStorage.getItem('isLogin') → null → isLogin = false
!isLogin === true → 进入拦截分支
return <Navigate replace to="/login"
state={{ from: { pathname: "/pay", ... } }} />
T=4ms Navigate 组件渲染 → 触发导航:
hash 变为 '#/login'
history.replaceState(replace 模式)
将 state 写入历史记录
T=5ms hashchange 触发 → HashRouter 更新 → React 重渲染
T=6ms Routes 重新匹配:
path="/login" → ✔ 匹配!
渲染 <Login />
T=7ms Login 组件执行:
location = useLocation()
location.state = { from: { pathname: "/pay", ... } }
from = location.state?.from?.pathname || '/'
from = "/pay"
console.log("/pay", 'from') // DevTools 可见
T=8ms 用户看到登录表单,输入用户名和密码
T=5000ms 用户点击"登录"按钮
T=5001ms handleSubmit 执行:
e.preventDefault()
formData.get("username") → "admin"
formData.get("password") → "123456"
验证通过!
localStorage.setItem('isLogin', 'true')
navigate("/pay", { replace: true })
T=5002ms hash 变为 '#/pay'(replace 模式,将 /login 从历史栈中替换掉)
历史栈当前状态:[首页] → [/pay]
T=5003ms hashchange 触发 → HashRouter 更新 → React 重渲染
T=5004ms Routes 重新匹配 /pay → 再次渲染 ProtectRoute
T=5005ms ProtectRoute 再次执行:
localStorage.getItem('isLogin') → 'true' → isLogin = true
isLogin === true → 走正常分支!
return <>ProtectRoute: {children}</>
children = <Pay />
渲染 <Pay />
T=5006ms 用户看到支付页面:
"支付页面"
"天龙八部 RAG 助手 - 年度会员"
"¥99.00"
[微信支付] [支付宝]
[确认支付 ¥99.00]
[退出登录]
T=8000ms 用户选择"支付宝",点击"确认支付"
T=8001ms handlePay 执行:
setPaying(true) → 按钮变灰,显示"支付中..."
setTimeout 1500ms
T=9500ms setTimeout 回调执行:
setPaying(false)
setPaySuccess(true)
T=9501ms 组件重新渲染,paySuccess === true:
显示"支付成功"
"订单号:ORD1691..."
"金额:¥99.00"
"即将跳转首页..."
setTimeout 2000ms
T=11500ms setTimeout 回调执行:
navigate('/', { replace: true })
T=11501ms hash 变为 '#/' → React 重渲染 → 匹配到 path="/" → 渲染 <Home />
用户回到首页,整个流程结束。
4.13.3 关键数据流总结
javascript
localStorage location.state
──────────── ──────────────
ProtectRoute 拦截: 读取 isLogin 写入 { from: location }
(判断是否放行) (记住"用户从哪来")
Login 登录成功: 写入 isLogin='true' 读取 from.pathname
(标记已登录) (知道"该回哪去")
Pay 退出登录: 删除 isLogin (不涉及)
(清除登录状态)
全程数据的两个"隐式通道":
- localStorage:浏览器存储,跨页面/跨刷新持久化,用于判断登录状态
- location.state:路由状态,仅存在于当前导航会话的历史栈中,不持久化