摘要 手把手实现路由鉴权守卫,逐行拆解 ProtectRoute、localStorage、Navigate state、useLocation、FormDat

今日重点速览

今天学了 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>,不需要改任何页面内部的代码。


这个知识点在讲什么

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> 除了 toreplace,还有一个 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?.d vs a && 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 |


学习自查清单

  1. children 模式的核心价值是什么?能举出一个非路由场景的例子吗?--- 见「children 定制化」
  2. ProtectRoute 组件里做了哪两个分支判断?各自的返回值是什么?--- 见「ProtectRoute」
  3. localStorage 存 boolean 有什么坑?怎么避免?--- 见「localStorage」
  4. location.state 什么时候是 null,什么时候有值?--- 见「Location.state 与两道保险」
  5. ?.|| 分别在防什么?--- 见「Location.state 与两道保险」
  6. e.currentTargete.target 有什么区别?FormData 为什么需要 currentTarget?--- 见「Login 表单」
  7. FormData 按什么规则提取表单数据?需要额外配置吗?--- 见「Login 表单」
  8. ProtectRoute 的 replace 和 Login 的 replace 各管什么?--- 见「navigate 的 replace」
  9. 如果用户登录后点后退,会回到登录页吗?为什么?--- 见「navigate 的 replace」
  10. 完整鉴权链路中,数据从 ProtectRoute 传到 Login 再到跳回,经历了哪些变量和 API?--- 见「完整链路串联」

#React #ReactRouter #鉴权 #前端路由 #localStorage

相关推荐
肉肉不吃 肉1 小时前
无渲染组件和组合式函数
前端·javascript·vue.js
其美杰布-富贵-李1 小时前
01 从项目结构开始认识 Vue 3
javascript·vue.js·ecmascript
sugar__salt1 小时前
前端路由技术详解:从传统多页到 React SPA 的完整进化之路
前端·javascript·react.js·前端框架
用户84298142418101 小时前
JS代码压缩实测:可减小体积、提高执行效率!
前端·javascript·后端
Synmbrf2 小时前
ECharts 饼图鼠标悬浮后标签 title 消失的问题
前端·javascript
breeze jiang3 小时前
React useRef 实战:从 input 聚焦到 Web Worker 引用
前端·javascript·react.js
苏灿烤鱼4 小时前
GitHub #2 拆解|把工程经验装进 Agent,为什么仍会“静默失效”?
javascript·人工智能·agent
To_OC12 小时前
LC 35 搜索插入位置:二分查找谁都会,边界条件谁写谁懵
javascript·算法·leetcode
码哥DFS12 小时前
二叉树的直径
开发语言·javascript·算法