今日重点速览
今天学了 React Router 的鉴权守卫(ProtectRoute),核心目标是:某些页面需要登录才能访问。实现这条链路涉及 11 个 API 和概念,分为三大块:
门禁端(ProtectRoute) :children 定制模式、localStorage 判断登录状态、useLocation 读 URL、Navigate state 隐式传参
登录端(Login) :useLocation().state 收参数、?. 可选链 + || 兜底、FormData 提取表单、e.preventDefault 阻止默认提交、navigate replace 跳回
概念背景:readme 鉴权路由设计、Modal 的 children 定制模式、localStorage key-value 存储、浏览器原生 FormData API
概念依赖链:
scss
children 模式 → ProtectRoute 组件 → localStorage 判断 → Navigate state 传参
↓
FormData 提取 ← e.preventDefault 阻止 ← Login 表单渲染 ← useLocation().state 接收
↓
navigate replace → 回到被保护页面
一、鉴权路由的设计背景 --- 锚点来源: readme
前置依赖: 无,这是今天课程的总纲。
这个知识点在讲什么
readme 写了几条关于鉴权的笔记:
markdown
## 鉴权路由
- http 无状态的
- 有状态
- 请求头 token Authorization
- Cookie
- localStorage 存储登录状态
user admin
password 123456
HTTP 协议是无状态的------服务器不记得你上次请求做了什么。每次请求都是独立的。要实现"记住登录状态",就需要额外的机制:
- token :放在请求头
Authorization里,每次请求带上,服务器验证 - Cookie:浏览器自动携带的小数据,常用于 session
- localStorage:浏览器本地存储,纯前端读写,今天的主角
今天实现的是最简版本:用 localStorage 存一个登录标记,在前端路由层面做拦截。真实项目里还需要后端 token 验证,但路由守卫这一层的逻辑是一样的。
账号是 admin,密码是 123456,写死在 Login 组件里做演示。
为什么要这样设计
把鉴权逻辑抽成独立组件,而不是在每个页面里写 if (isLogin):改一处生效所有页面,新加一个要保护的页面只需套一层 <ProtectRoute>,不需要改任何页面内部的代码。
二、children 定制化:从 Modal 到 ProtectRoute --- 锚点来源: readme + 代码注释
这个知识点在讲什么
readme 接着写了 Modal 组件的设计思路:
xml
- 组件内部的子组件
props.children 拿到组件申明的内部所有的子节点
modal 弹窗组件 mask蒙层
窗体 头部 尾部 主题部分children 传入
定制性高
<Modal>
{children 定制}
</Modal>
Modal 弹窗的结构是固定的:mask 蒙层(半透明背景)、窗体(白色框)、头部和尾部。只有主体部分每次不同------有时是确认文字,有时是输入表单,有时是图片预览。所以把可变的主体做成 children,谁用谁定制。
ProtectRoute 和 Modal 是同一个套路:
| 固定部分 | 可变部分(children) | |
|---|---|---|
| Modal | mask、header、footer | 主体内容 |
| ProtectRoute | 鉴权检查逻辑 | 被保护的页面 |
代码里怎么体现的
App.jsx 第 50-58 行:
javascript
<Route path="/pay" element={
// 门禁保安
// pay 要进去的页面
// children 用来定制化组件
<ProtectRoute>
{/* children */}
<Pay />
</ProtectRoute>
} />
逐行拆解
javascript
// App.jsx:50-58
<Route path="/pay" element={...} />
// ↑ /pay 路径匹配后,不是直接渲染 <Pay />,而是进 element={...}
// App.jsx:51 --- // 门禁保安
// ↑ ProtectRoute 的角色:像小区门口的保安,拦下每个人检查
// App.jsx:52 --- // pay 要进去的页面
// ↑ Pay 是被保护的"住户",只有通过检查才能进
// App.jsx:53 --- // children 用来定制化组件
// ↑ 把 Pay 放在 ProtectRoute 标签之间,作为 children 传入
// ↑ 好处:以后要保护 Admin,写 <ProtectRoute><Admin /></ProtectRoute> 就行,保安逻辑完全复用
<ProtectRoute>
{/* children */}
<Pay />
</ProtectRoute>
// ↑ Pay 作为 children 传给 ProtectRoute,在内部通过 return children 放行
为什么要这样设计
写死 vs children 的对比:
javascript
// 写死:只能保护一个页面
function ProtectRoute() {
if (!isLogin) return <Navigate to="/login" />
return <Pay /> // ← Pay 写死在组件里了
}
// children:保护谁都行
function ProtectRoute({ children }) {
if (!isLogin) return <Navigate to="/login" />
return children // ← 塞什么保护什么
}
一句话:children 让 ProtectRoute 从"Pay 的专属保安"变成了"谁都可以用的通用保安"。
学到这会想问: 如果有多个子组件呢?children 还能用吗?
解答 :可以。
<ProtectRoute><Header /><Content /><Footer /></ProtectRoute>这样 children 就是[<Header />, <Content />, <Footer />]数组。React 在渲染{children}时会自动逐个渲染数组里的元素。<Router>标签里就同时包了<Navigation />和<Routes>,原理完全一样。
知识点卡片
| 掌握项 | 要点 |
|---|---|
| children 本质 | props.children,标签之间的内容会自动传入 |
| 为什么用 children | 壳(保安逻辑)复用,内容(被保护的页面)定制 |
| 与 Modal 的相似性 | 都是固定外壳 + 可变 children |
三、ProtectRoute:路由守卫的门禁逻辑 --- 锚点来源: 代码注释 + 代码逻辑
前置依赖: 理解了 children 模式(第二章)。
这个知识点在讲什么
ProtectRoute 是一个 React 组件,它接收 children,检查登录状态:
- 已登录 → 渲染 children(放行)
- 未登录 → 渲染
<Navigate>(踢去登录页)
代码里怎么体现的
ProtectRoute.jsx 完整代码:
javascript
// ProtectRoute.jsx:1-4
import {
Navigate,
useLocation
} from 'react-router-dom'
// ProtectRoute.jsx:6-24
const ProtectRoute = ({ children }) => {
console.log(children, '-------')
const location = useLocation()
// 拦截请求 鉴权
// html5 本地存储 域名的沙盒环境
const isLogin = localStorage.getItem('isLogin') === 'true'
if (!isLogin) {
// 如果没有登录,跳转到登录页面
// 路由 设置state状态对象
return <Navigate to="/login" replace state={{ from: location.pathname }} />
}
return (
<>
{children}
</>
)
}
export default ProtectRoute
逐行拆解
arduino
// 第1行: import { Navigate, useLocation }
// ↑ 从 react-router-dom 引入两个东西:
// Navigate: 声明式重定向组件
// useLocation: Hook,读取当前 URL 信息
// 第6行: const ProtectRoute = ({ children }) =>
// ↑ ({ children }) 是解构赋值,从 props 对象里拆出 children 属性
// ↑ 等价于:const children = props.children
// ↑ children 就是 <Pay />(App.jsx 里包在标签之间的内容)
// 第7行: console.log(children, '-------')
// ↑ 调试用,打印 children 的内容,确认传进来了
// 第8行: const location = useLocation()
// ↑ useLocation() 是个 Hook,自动读取浏览器当前地址栏的 URL
// ↑ 返回 { pathname: '/pay', search: '', hash: '', state: null }
// ↑ 不用传任何参数,完全自动
// 第9-10行: // 拦截请求 鉴权 / // html5 本地存储 域名的沙盒环境
// ↑ "拦截请求":在用户进入页面之前就做判断,不等到页面渲染
// ↑ "域名的沙盒环境":每个网站有自己的 localStorage 命名空间,互相隔离
// baidu.com 的 localStorage 和 taobao.com 的 localStorage 互不可见
// 第11行: const isLogin = localStorage.getItem('isLogin') === 'true'
// ↑ localStorage.getItem(key): 从浏览器本地存储读取
// ↑ 没存过 → 返回 null
// ↑ "true" === 'true' → true(已登录)
// ↑ null === 'true' → false(未登录)
// ↑ 为什么比较字符串?因为 localStorage 只能存字符串
// 第13-16行: if (!isLogin) { return <Navigate ... /> }
// ↑ !isLogin 即未登录
// ↑ <Navigate to="/login">:重定向到登录页
// ↑ replace: 替换历史记录,/pay 的痕迹被抹掉
// ↑ state={{ from: location.pathname }}: 把当前路径 /pay 藏在 state 里传给 Login
// 等 Login 那边取出 state.from,登录后跳回来
// 第19-22行: return (<> {children} </>)
// ↑ 走到这里说明 isLogin = true,已登录
// ↑ 把 children(即 <Pay />)渲染出来
// ↑ <>...</> 是 React Fragment 简写,不生成额外的 DOM
为什么要这样设计
为什么不在每个页面里写 if 判断?
javascript
// 坏做法:每个页面自己判断
function Pay() {
const isLogin = localStorage.getItem('isLogin') === 'true'
if (!isLogin) return <Navigate to="/login" />
return <h1>支付</h1>
}
function Admin() {
const isLogin = localStorage.getItem('isLogin') === 'true'
if (!isLogin) return <Navigate to="/login" />
return <h1>管理</h1>
}
// 重复代码、重复逻辑、改一处漏一处
javascript
// 好做法:守卫抽成组件
<ProtectRoute><Pay /></ProtectRoute>
<ProtectRoute><Admin /></ProtectRoute>
// 逻辑集中,改一处全部生效
为什么 Navigate 而不是手动改 URL?
javascript
// 手动改:绕过了 React Router,Routes 不会自动重新匹配
window.location.hash = '/login' // ❌
// Navigate 组件:React Router 内部处理,Routes 重新匹配
<Navigate to="/login" replace state={...} /> // ✅
知识点卡片
| 掌握项 | 要点 |
|---|---|
| children 传入方式 | ({ children }) 从 props 解构 |
| 鉴权分支 | if (!isLogin) 拦,否则放 |
| Navigate 重定向 | 改 URL + Routes 重新匹配 |
| state 传参 | state={{ from: location.pathname }} |
| replace | 替换历史记录,不留 /pay 痕迹 |
四、localStorage:登录状态的持久化 --- 锚点来源: readme + 代码注释
前置依赖: ProtectRoute 组件(第三章),理解 isLogin 变量从哪来。
这个知识点在讲什么
localStorage 是浏览器提供的本地存储 API,数据存在硬盘上,关了浏览器再打开还在。它和 this.routers 对象一样是 key-value 存储,但有三个关键区别:
this.routers(对象) |
localStorage |
|
|---|---|---|
| 存在哪 | 内存,刷新就没了 | 硬盘,刷新还在 |
| 存什么 | 任意 JavaScript 值 | 只能是字符串 |
| 存取方式 | obj[key] = value / obj[key] |
setItem(key, value) / getItem(key) |
代码里怎么体现的
javascript
// ProtectRoute.jsx:11 --- 读取
const isLogin = localStorage.getItem('isLogin') === 'true'
// Login/index.jsx:25 --- 写入
localStorage.setItem('isLogin', 'true')
// 退出登录时 --- 删除
localStorage.removeItem('isLogin')
逐行拆解
sql
// 存(在 Login 里)
localStorage.setItem('isLogin', 'true')
// key → 'isLogin', value → 'true'
// ↑ 把 true 存进去,但浏览器会转成字符串 "true"
// ↑ 所以这里写 true 还是 "true" 都是存 "true"
// 取(在 ProtectRoute 里)
localStorage.getItem('isLogin')
// ↑ 拿到了字符串 "true" → 和字符串 "true" 比较 → true
// ↑ 拿到了 null(没存过)→ null === 'true' → false
为什么用 === 'true' 而不是直接判断
ini
// 危险写法
const isLogin = localStorage.getItem('isLogin') // → "true" 或 "false" 或 null
if (isLogin) { ... }
// 如果值是 "false" → "false" 是 truthy 值!→ if 判断为 true,不管用
// 安全写法
const isLogin = localStorage.getItem('isLogin') === 'true'
// "true" === 'true' → true ✅
// "false" === 'true' → false ✅
// null === 'true' → false ✅
localStorage 只能存字符串的坑,必须精确比较。
为什么 localStorage 刷新不丢
localStorage 存在浏览器的磁盘存储里,和数据表、cookie、indexedDB 共享同一个"网站存储配额"。它的生命周期独立于浏览器标签页的生命周期------关闭标签页、关闭浏览器、甚至重启电脑,数据都在。只有 localStorage.clear() 或 removeItem() 或用户手动清理浏览器数据才会消失。
知识点卡片
| 掌握项 | 要点 |
|---|---|
| 存 | localStorage.setItem('key', 'value') |
| 取 | localStorage.getItem('key') → 字符串或 null |
| 删 | localStorage.removeItem('key') |
| 陷阱 | 只能存字符串,布尔值要显式 === 'true' 转换 |
| 生命周期 | 永久,直到手动删除 |
| 本质 | key-value 存储,和普通对象存取方式不同但逻辑一样 |
五、useLocation:自动读取当前 URL --- 锚点来源: 代码逻辑
这个知识点在讲什么
useLocation() 是 React Router 提供的一个 Hook。和 useParams 类似,它自动读取当前 URL 信息。区别在于:
useParams |
useLocation |
|
|---|---|---|
| 拿什么 | Route path 里 : 定义的动态参数 |
URL 完整信息 |
| 依赖 Route 预定义? | 需要 path=":id" |
不需要 |
| 返回 | { id: '123' } |
{ pathname, search, hash, state, key } |
代码里怎么体现的
scss
// ProtectRoute.jsx:8
const location = useLocation()
// 当前 URL: http://localhost:5174/pay
// location = {
// pathname: '/pay', ← 路径
// search: '', ← ? 查询参数
// hash: '', ← # 后面的
// state: null, ← Navigate 传的隐式数据
// key: 'abc123' ← 唯一标识
// }
location.pathname 就是当前路径 /pay,不需要手动拼字符串,也不需要从 Route 配置里取值。ProtectRoute 用它来记录"用户是从哪个页面被踢走的",以便登录后跳回来。
和 useParams 的使用场景对比
arduino
// useParams:从 URL 路径中提取预定义的占位符
<Route path="/user/:id" /> → useParams() → { id: '123' }
// 需要 Route 预先声明 :id
// useLocation:不管有没有 Route,都能读 URL
useLocation().pathname → '/pay'
// 不需要任何 Route 配合
知识点卡片
| 掌握项 | 要点 |
|---|---|
| 返回什么 | { pathname, search, hash, state, key } |
| 需要参数吗 | 不需要,自动读 |
| 和 useParams 区别 | useParams 需要 Route 声明 :xxx,useLocation 不需要 |
| 使用场景 | 需要知道当前完整 URL 时用(鉴权重定向、面包屑等) |
六、Navigate 的 state:隐式传参 --- 锚点来源: 代码注释 + 代码逻辑
这个知识点在讲什么
<Navigate> 除了 to 和 replace,还有一个 state prop。它允许在跳转时附带数据,数据不会出现在 URL 上。
代码里怎么体现的
javascript
// ProtectRoute.jsx:16
return <Navigate to="/login" replace state={{ from: location.pathname }} />
// ─────────┬─────────
// 这个对象不会出现在 URL 上
逐行拆解
css
state={{ from: location.pathname }}
// ① 外层 {}:JSX 的值表达式
// ② 内层 {}:JavaScript 对象字面量
// ③ 整体等于:state={ { from: '/pay' } }
// 传给 Navigate 的 state prop 值 = { from: '/pay' }
和查询参数对比
ini
// 方式一:查询参数(暴露在 URL 上)
<Navigate to="/login?from=/pay" />
// URL: /login?from=/pay ← 用户能看到,能手动改
// 方式二:state(隐式传参)
<Navigate to="/login" state={{ from: '/pay' }} />
// URL: /login ← 干干净净,用户看不到来源
state 通过浏览器的 history.state 存储,不在 URL 上暴露。适合传来源页面、表单中间态等不需要持久化的数据。注意刷新页面 state 会丢失。
知识点卡片
| 掌握项 | 要点 |
|---|---|
| state 是什么 | Navigate 的 prop,跳转时附带隐式数据 |
| vs 查询参数 | state 不在 URL 上,用户看不到也改不了 |
| 限制 | 刷新丢失,需要持久化请用 search 参数或 localStorage |
七、Location.state 与两道保险:?. + || --- 锚点来源: 代码逻辑
前置依赖: Navigate state 传参(第六章)。
这个知识点在讲什么
Login 页需要接收 ProtectRoute 通过 Navigate 传过来的 state。但这个 state 不一定有值------直接打开 /login 时 state 就是 null。所以需要两层保护。
代码里怎么体现的
csharp
// Login/index.jsx:10-11
const location = useLocation()
const from = location.state?.from || '/'
逐行拆解
perl
// location.state
// ↑ 来自 ProtectRoute 的 Navigate state 传参
// ↑ 如果有经过 Navigate:state = { from: '/pay' }
// ↑ 如果直接打开 /login:state = null
// ?.
// ↑ 可选链操作符
// ↑ location.state 是 null → 不继续访问 .from,直接返回 undefined
// ↑ location.state 有值 → 访问 .from,拿 '/pay'
// ↑ 不用 ?. 的话:null.from → TypeError,页面白屏
// || '/'
// ↑ 短路求值:左边是假值就取右边
// ↑ undefined || '/' → '/'(没 state 就默认回首页)
// ↑ '/pay' || '/' → '/pay'(有 state 就用 state)
state 什么时候是 null,什么时候有值
csharp
// 有值:从 ProtectRoute 的 Navigate 跳过来
<Navigate to="/login" state={{ from: '/pay' }} />
// useLocation().state → { from: '/pay' } ✅
// null:
// ① 直接地址栏输入 /login → state 未初始化 → null
// ② 从 <Link> 跳过来 → Link 没传 state → null
// ③ 刷新页面 → history.state 丢失 → null
四种情况验证
ini
state 存在,有 from → from = '/pay' ← 正常鉴权流程
state 存在,没 from → from = '/' ← 兜底
state 是 null → from = '/' ← 兜底
直接打开 /login → from = '/' ← 兜底
追问弧线
学到这会想问 :
?.和&&有什么区别?为什么这里用?.而不是&&?解答 :
?.是新版 JS 的可选链语法,专门用于"前面可能是 null/undefined"的场景。&&是老版写法:location.state && location.state.from。两者在这个场景下结果一样,但?.在深层嵌套时优势明显------a?.b?.c?.dvsa && a.b && a.b.c && a.b.c.d。现代 React 项目推荐用?.。
知识点卡片
| 掌握项 | 要点 |
|------------------|------------------------------------------|----|---------------|
| ?. | 可选链,左值为 null/undefined 时返回 undefined 不报错 |
| ` | | ` | 兜底默认值,左侧假值取右侧 |
| state 有值的情况 | Navigate 传了 state prop |
| state 为 null 的情况 | 直接访问、Link 跳转、刷新页面 |
八、Login 表单:e.preventDefault + FormData + e.currentTarget --- 锚点来源: 代码注释 + 代码逻辑
前置依赖: 理解了 state 参数(第七章),from 变量已经有了。
这个知识点在讲什么
这是今天新增内容中最核心的一组原生 API 配合。Login 组件的表单部分涉及三个独立概念:
e.preventDefault():阻止表单默认提交行为e.currentTarget:拿到绑事件的 DOM 元素FormData:浏览器原生 API,按name属性提取表单数据
代码里怎么体现的
csharp
// Login/index.jsx:14-31
const handleSubmit = (e) => {
e.preventDefault() // 阻止浏览器默认提交
const formData = new FormData(e.currentTarget) // 从表单创建数据对象
const username = formData.get('username') // 按 name 取值
const password = formData.get('password')
if (!username || !password) {
alert('请输入用户名和密码')
return // 空值,不往下走
}
if (username === 'admin' && password === '123456') {
alert('登录成功')
localStorage.setItem('isLogin', 'true')
navigate(from, { replace: true }) // 跳回原页面
} else {
alert('用户名或密码错误')
}
}
JSX 部分:
xml
// Login/index.jsx:36-51
<form onSubmit={handleSubmit}>
<h1>登录</h1>
<input name="username" placeholder="请输入用户名" required />
<input name="password" placeholder="请输入密码" required />
<button type="submit">登录</button>
</form>
逐行拆解
arduino
// 第14-15行: const handleSubmit = (e) => { e.preventDefault() }
// ↑ e 是表单提交事件对象
// ↑ e.preventDefault() 阻止浏览器的默认表单行为
// ↑ 默认行为:刷新页面 + 把表单数据发给服务器 ------ SPA 不能这样
// ↑ 和 <Link> 的 e.preventDefault() 是同一个概念
// Link 拦的是 <a> 的跳转,这里拦的是 <form> 的刷新
// 第16行: const formData = new FormData(e.currentTarget)
// ↑ e.currentTarget 是绑 onSubmit 的元素 → <form>
// ↑ FormData 是浏览器原生 API,不需要 npm install
// ↑ new FormData(form元素) 自动扫描 form 内所有带 name 属性的控件
// ↑ 不管有几个 input、select、textarea,一把全收集
// 第17-18行: formData.get('username') / formData.get('password')
// ↑ FormData 按 name 属性取值
// ↑ <input name="username"> → get('username')
// ↑ <input name="password"> → get('password')
// ↑ 这是浏览器定的规矩,name 属性就是 FormData 的 key
e.currentTarget vs e.target
xml
<form onSubmit={handleSubmit}>
<input name="username" />
<button type="submit">登录</button> ← 用户点这里
</form>
e.currentTarget → <form> 整个表单(onSubmit 绑在谁身上)
e.target → <button> 实际被点的元素(用户点了哪)
这里用 e.currentTarget 因为 FormData 需要一个 <form> 元素来扫描里面所有的 input。如果用 e.target,万一用户是通过键盘回车提交的,e.target 可能是某个 <input>,传给 FormData 的还是应该永远是整个 <form>。
FormData 按 name 扫描是谁的规则
这是浏览器 HTML5 规范定的。FormData 构造函数会遍历传入的 <form> 元素的所有子控件,把每个带 name 属性的控件的值收集起来。name 就是 key,控件的当前值就是 value。你不是在"配置"这个行为,它天生就这样工作。
状态对照表
| 条件 | 结果 |
|---|---|
| 用户名为空 | alert + return,不往下走 |
| 密码错误 | alert + 不存 localStorage + 不跳转,留在登录页 |
| 用户名 admin + 密码 123456 | alert + localStorage 存标记 + navigate 跳回 |
| 从 ProtectRoute 来 | 跳回原页面(如 /pay) |
| 直接打开 /login | 跳回首页(from 兜底为 /) |
知识点卡片
| 掌握项 | 要点 |
|---|---|
| e.preventDefault | 阻止表单默认提交刷新 |
| e.currentTarget | 绑事件的 DOM 元素 |
| new FormData(form) | 浏览器原生 API,按 name 提取 |
| formData.get(name) | 按 name 属性取值 |
| required 属性 | HTML5 表单增强,浏览器的非空校验 |
九、navigate 的 replace:两个 replace 各管什么 --- 锚点来源: 代码注释 + 代码逻辑
这个知识点在讲什么
鉴权链路中出现了两个 replace:
bash
// ① ProtectRoute.jsx:16 --- 从 /pay 踢向 /login 时
<Navigate to="/login" replace state={{ from: location.pathname }} />
// ② Login/index.jsx:28 --- 从 /login 跳回 /pay 时
navigate(from, { replace: true })
两个 replace 各管什么
第一个 replace(ProtectRoute) :把 /pay 从历史记录中替换成 /login。
bash
替换前: [首页, /pay] ← /pay 在历史里
替换后: [首页, /login] ← /pay 被 /login 替换了
去掉 /pay 的原因:用户在 /login 点后退,回的是首页;如果 /pay 还在历史里,点后退回 /pay,又被踢去 /login,陷入无限重定向循环。
第二个 replace(Login) :把 /login 从历史记录中替换成 /pay。
bash
替换前: [首页, /login] ← /login 在历史里
替换后: [首页, /pay] ← /login 被 /pay 替换了
去掉 /login 的原因:用户跳到 /pay 后点后退,直接回首页,不会回到已经完成的登录页。
最终效果
两个 replace 把登录这个过程从浏览历史里完全抹掉:
bash
实际经历: /pay → 被踢 → /login → 登录成功 → /pay
历史记录: [首页, /pay] ← 只有这两条,中间过程消失了
replace 替换的是"当前这一条"历史记录,不是整个历史栈。 每次 replace: true 都只替换当前所在的那一条。
知识点卡片
| 掌握项 | 要点 |
|---|---|
| ProtectRoute 的 replace | /pay 被替换成 /login |
| Login 的 replace | /login 被替换成 /pay |
| 共同效果 | 登录过程不出现在历史记录中 |
| 替换范围 | 只是当前这一条,不是全部 |
十、完整鉴权链路串联 --- 锚点来源: 代码逻辑
前置依赖: 前面九章的全部概念。
全流程时间线
ini
t=0 用户点击了 <Link to="/pay">支付</Link>
t=0 Link 内部 e.preventDefault → 改 URL → URL 变 /pay
t=0 Routes 匹配 path="/pay" → 进入 <ProtectRoute>
t=0 useLocation().pathname → '/pay'
t=0 localStorage.getItem('isLogin') → null → isLogin = false
t=0 return <Navigate to="/login" replace state={{ from: '/pay' }} />
↓ Navigate 执行
t=0 URL 变 /login,replace 把 /pay 替换掉
t=0 Routes 重新匹配 → path="/login" → 渲染 <Login />
t=0 useLocation().state → { from: '/pay' }
t=0 from = location.state?.from || '/' → '/pay'
t=0 console.log('/pay') ← 控制台输出
t=0 渲染表单(用户名 + 密码输入框 + 登录按钮)
── 用户操作 ──
t=3s 输入用户名:admin,密码:123456
t=3s 点"登录" → handleSubmit 触发
t=3s e.preventDefault() ← 阻止浏览器默认表单提交
t=3s new FormData(e.currentTarget) ← 从表单收集数据
t=3s formData.get('username') → 'admin'
t=3s formData.get('password') → '123456'
t=3s username === 'admin' && password === '123456' → true
t=3s alert('登录成功') ← 弹出提示
t=4s 用户点确定
t=4s localStorage.setItem('isLogin', 'true') ← 写入硬盘
t=4s navigate('/pay', { replace: true }) ← 跳回 /pay,replace 替换掉 /login
t=4s URL 变 /pay
t=4s Routes 匹配 path="/pay" → <ProtectRoute>
t=4s localStorage.getItem('isLogin') → 'true'
t=4s isLogin = 'true' === 'true' → true
t=4s !isLogin → false → 不拦截
t=4s return children → 渲染 <Pay />
t=4s 用户看到 <h1>支付</h1>
数据流总览
csharp
ProtectRoute Login
useLocation() useLocation()
↓ ↓
location.pathname = '/pay' location.state = { from: '/pay' }
↓ ↓
state={{ from: '/pay' }} from = '/pay'
↓ ↓
Navigate ─────────────────────→ 登录表单 Fill
↓
localStorage.setItem('isLogin', 'true')
navigate('/pay', { replace: true })
↓
────→ 回到 ProtectRoute
↓
isLogin = true → 放行 → Pay
涉及的全部 API
javascript
组件 所在文件
─────────────────────────────────────────
<ProtectRoute> App.jsx / ProtectRoute.jsx
<Navigate> ProtectRoute.jsx
useLocation ProtectRoute.jsx / Login.jsx
useNavigate Login.jsx
localStorage.setItem Login.jsx
localStorage.getItem ProtectRoute.jsx
FormData Login.jsx
e.currentTarget Login.jsx
e.preventDefault Login.jsx
?. Login.jsx
|| Login.jsx
代码演进回顾
鉴权链路的功能演进:
| 功能 | v1(早期无守卫) | v2(加 ProtectRoute) | v3(加 state 传递) | v4(加 replace) |
|----------|-----------|--------------------|-------------------------|---------------------|---------|------|
| Pay 页面 | 直接访问,无门禁 | 被 ProtectRoute 包裹 | 拦截后传 state.from | replace 清理历史 |
| Login 页面 | 无状态接收 | 无状态接收 | 用 location.state 收 from | navigate replace 跳回 |
| 登录状态 | 无持久化 | localStorage 存取 | 同 v2 | 同 v2 |
| 错误处理 | 无 | 无 | ?. + ` | | ` 双层兜底 | 同 v3 |
学习自查清单
- children 模式的核心价值是什么?能举出一个非路由场景的例子吗?--- 见「children 定制化」
- ProtectRoute 组件里做了哪两个分支判断?各自的返回值是什么?--- 见「ProtectRoute」
- localStorage 存 boolean 有什么坑?怎么避免?--- 见「localStorage」
location.state什么时候是 null,什么时候有值?--- 见「Location.state 与两道保险」?.和||分别在防什么?--- 见「Location.state 与两道保险」e.currentTarget和e.target有什么区别?FormData 为什么需要 currentTarget?--- 见「Login 表单」- FormData 按什么规则提取表单数据?需要额外配置吗?--- 见「Login 表单」
- ProtectRoute 的 replace 和 Login 的 replace 各管什么?--- 见「navigate 的 replace」
- 如果用户登录后点后退,会回到登录页吗?为什么?--- 见「navigate 的 replace」
- 完整鉴权链路中,数据从 ProtectRoute 传到 Login 再到跳回,经历了哪些变量和 API?--- 见「完整链路串联」
#React #ReactRouter #鉴权 #前端路由 #localStorage