React Router 进阶:路由守卫、登录鉴权与状态传递

React Router 进阶:路由守卫、登录鉴权与状态传递

前言

上一篇文章我们搭建了 React Router v6 的基本骨架------路由匹配、懒加载、动态参数、嵌套路由和 404 兜底。但那还是一个"敞开大门"的应用:任何人随便敲个 URL 就能访问所有页面。

真实项目里,有些页面需要登录才能看 (支付页、个人中心、后台管理)。这就引出了今天的主角:路由守卫(Route Guard)登录鉴权 以及在这个过程中 React Router 如何在不同页面之间传递状态


一、HashRouter vs BrowserRouter:彻底搞清楚两种路由

javascript 复制代码
// App.jsx ------ 今天从 HashRouter 换到了 BrowserRouter
import {
  BrowserRouter as Router,  // ← 换了!
  Routes,
  Route,
  Navigate,
} from 'react-router-dom';

HashRouter 是怎么工作的

URL 里 # 后面的内容叫 fragment(片段标识符) ,它的核心特性是:改变 hash 不会触发浏览器向服务器发请求

bash 复制代码
http://localhost:5173/#/pay
                      └────────── hash 部分,浏览器不管它

当你从 #/home 切到 #/pay

  1. 浏览器不发起 HTTP 请求
  2. 但触发 hashchange 事件
  3. React Router 监听这个事件,根据 hash 值切换组件

优点 :零配置,服务器不需要任何配合。扔到任何静态服务器上都能跑。 缺点 :URL 里有个 #,不够"干净",看起来不像传统网页。

BrowserRouter 是怎么工作的

BrowserRouter 依赖的是 HTML5 引入的 History API

javascript 复制代码
// 底层原理
window.history.pushState(null, '', '/pay');  // URL 变了,页面不刷新

它可以直接操作完整的 URL 路径,不需要 #

bash 复制代码
http://localhost:5173/pay   ← 干净的 URL,没有 #

但有一个关键前提 :当用户直接访问 /pay 或刷新页面 时,浏览器会向服务器发起 GET 请求。服务器必须配置一条规则------所有路径都返回 index.html,让前端 React Router 来接管路由匹配。

bash 复制代码
用户刷新 /pay → 服务器请求 GET /pay
  ├─ 没配置 fallback → 服务器返回 404
  └─ 配置了 fallback → 服务器返回 index.html → React Router 接管 → 匹配 /pay 渲染组件

Vite 开发模式下默认已配置好,生产环境需在 Nginx 中加:

bash 复制代码
location / {
    try_files $uri /index.html;
}
HashRouter BrowserRouter
URL 样子 /#/pay /pay
原理 hashchange 事件 History API(pushState
服务器配置 不需要 需要 fallback 到 index.html
适用场景 Demo、GitHub Pages、静态部署 生产环境、SEO 友好

Demo 阶段用 HashRouter 省心,上线后切 BrowserRouter 更专业。


二、Restful 设计理念:URL 就是资源

在深入路由之前,先明确一个设计思想:一切皆资源

bash 复制代码
/products          → 商品列表资源
/products/123      → id=123 的商品资源
/products/new      → 创建新商品的入口资源
/user/123          → id=123 的用户资源
/pay               → 支付页面资源
/login             → 登录页面资源

每个 URL 路径对应一个逻辑上的资源,路由切换 = 访问不同资源,而不是"跳转到一个新页面"。

这也解释了什么前端路由要接管 URL------URL 是用户与应用交互的入口,它应该准确表达"当前在看什么"。


三、路由对象:前端路由的三个核心概念

浏览器暴露了三个核心对象,React Router 正是对它们做了 React 风格的封装:

原生概念 React Router Hook 作用
window.navigator (无直接 Hook,Link/Navigate 内部封装) 导航能力
window.location useLocation() 当前在哪个 URL
window.history useNavigate() 历史记录栈的操作
bash 复制代码
┌─────────────────────────────┐
│  浏览器                      │
│                             │
│  navigator  → "你能去哪"     │
│  location   → "你现在在哪"    │
│  history    → "你之前去过哪"  │
│                             │
│  React Router 把它们包装成   │
│  组件和 Hooks,让你在 JSX 中  │
│  自然地操作路由              │
└─────────────────────────────┘

理解这三个概念,就能理解为什么 Link 不刷新页面------它底层调用的是 history.pushState,只改 URL 不发请求;为什么 useLocation().state 能跨页面传数据------因为 pushState 本身就支持携带 state 对象。


四、路由守卫:用 {children} 做"门禁"

javascript 复制代码
// ProtectRoute.jsx
import { Navigate } from 'react-router-dom';

const ProtectRoute = ({ children }) => {
  // 从 localStorage 读取登录状态
  const isLogin = localStorage.getItem('isLogin') === 'true';

  if (!isLogin) {
    // 没登录 → 跳转到 /login,同时捎带上"从哪来的"
    return <Navigate to="/login" replace state={{ from: location.pathname }} />;
  }

  // 已登录 → 放行,渲染子组件
  return <>{children}</>;
};

设计模式:children 作为"插槽"

ProtectRoute 不关心自己被包裹的是什么组件,它只做一件事:检查权限 → 决定放行还是拦截 。具体渲染什么,完全由使用方通过 children 传入:

javascript 复制代码
// App.jsx
<Route path="/pay" element={
  <ProtectRoute>
    <Pay />          {/* ← children,ProtectRoute 不管它是什么 */}
  </ProtectRoute>
} />

这就是 React 组件化的精髓------关注点分离 。ProtectRoute 只关心"是否有权限",Pay 只关心"支付页长什么样",两者通过 children 松耦合地组合在一起。

children 本质上就是组件调用时写在 JSX 标签内部的所有内容:

xml 复制代码
<ProtectRoute>
  <Pay />           ← 这个就是 children
</ProtectRoute>

// 等价于
ProtectRoute({ children: <Pay /> })

五、Navigatestate 对象:跨页面传递"隐藏数据"

csharp 复制代码
<Navigate to="/login" replace state={{ from: location.pathname }} />
//                               └────────────────────────────────┘
//                          state 对象:URL 里看不见,目标页面能拿到

它解决了什么问题

用户在地址栏输入 #/pay,ProtectRoute 发现没登录,重定向到 /login。登录成功后,该跳回哪里?必须有人告诉 Login 页面"用户是从 /pay 过来的" 。这个信息不能放在 URL 里(会暴露内部路径,而且破坏 Login 页面的 URL 结构),但又需要在页面间传递------state 就是干这个的。

数据如何流转

ini 复制代码
用户访问 /pay(未登录)
  │
  ▼
ProtectRoute:
  <Navigate to="/login" state={{ from: '/pay' }} />
  │
  │  state 被写入 history.state
  │
  ▼
地址栏变成 /login,但 state 藏在 history 栈里
  │
  ▼
Login 组件:
  const location = useLocation();
  const from = location.state?.from || '/';   // → '/pay'
  │
  ▼
登录成功 → navigate(from) → 送回 /pay

为什么 state 刷新不丢

因为 React Router 底层调用的是 history.pushState(state, '', url)state 对象被浏览器序列化存储在历史记录栈中,页面刷新后从 history.state 恢复,所以 Login 页面刷新也依然能拿到 from


六、Login 页面:表单、状态存储、防回退

ini 复制代码
// Login/index.jsx
import { useNavigate, useLocation } from 'react-router-dom';

const Login = () => {
  const navigate = useNavigate();
  const location = useLocation();

  // 读取 ProtectRoute 传过来的"来源路径"
  const from = location.state?.from || '/';

  function handleSubmit(e) {
    e.preventDefault();  // 阻止表单的默认提交行为(会刷新页面)

    // FormData API:从 <form> 元素中提取用户输入
    const formData = new FormData(e.currentTarget);
    const username = formData.get('username');
    const password = formData.get('password');

    // 硬编码的鉴权(Demo 演示,生产环境需要后端验证)
    if (username === 'admin' && password === '123456') {
      localStorage.setItem('isLogin', 'true');
      navigate(from, { replace: true });  // ← 关键:replace 模式
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" placeholder="请输入用户名" required />
      <input name="password" placeholder="请输入密码" required />
      <button type="submit">登录</button>
    </form>
  );
};

FormData:不用受控组件也能拿表单数据

React 中处理表单通常用"受控组件"------每个 input 绑定一个 state:

scss 复制代码
// 受控组件写法(常见但繁琐)
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
<input value={username} onChange={e => setUsername(e.target.value)} />

FormData 是浏览器原生 API,直接从 <form> DOM 元素读取 name 属性对应的输入值,不需要 state、不需要 onChange。对于登录表单这种"提交时才关心值"的场景,简洁得多。

注意:input 必须加 name 属性,FormData 通过它来索引值。

bash 复制代码
历史记录栈(push 模式):
┌─────────┐
│  /      │  ← 初始页面
├─────────┤
│  /pay   │  ← 用户尝试访问
├─────────┤
│  /login │  ← 被重定向到登录页
└─────────┘
用户点"返回" → 回到 /pay → 又被 ProtectRoute 拦截 → 又跳到 /login
                                                      ↑
                                      死循环!用户被"困"在登录页
bash 复制代码
历史记录栈(replace 模式):
┌─────────┐
│  /      │  ← 初始页面
├─────────┤
│  /pay   │  ← /login 把这条记录替换掉了!
└─────────┘
用户点"返回" → 回到 / → 一切正常

replace: true 的作用:用新路径替换当前的历史记录 ,而不是追加一条新记录。登录成功后,/login 的历史记录就不该存在------用户不需要"回到登录页",回去只会被再次拦截。


七、localStorage:为什么用它存登录状态

javascript 复制代码
localStorage.setItem('isLogin', 'true');
localStorage.getItem('isLogin') === 'true';

HTTP 是无状态的

协议层面,每次 HTTP 请求都是独立的,服务器不会"记住"你上一次请求做了什么。网页要"记住"用户已登录,必须自己想办法保存状态。

三种常见方案

方案 存储位置 生命周期 用途
localStorage 浏览器磁盘 永久(除非手动清除) 登录状态、用户偏好
sessionStorage 浏览器内存 关闭标签页即清除 临时数据、表单草稿
Cookie 浏览器 + 每次请求自动发给服务器 可设置过期时间 Session Token

Demo 中用了最简单的 localStorage------它按域名 隔离存储(同一个域名的所有页面共享,不同域名的互相看不到),React Router 的 ProtectRoute 和 Login 在同一个域名下,所以 Login 写入的 isLogin,ProtectRoute 能读到。

makefile 复制代码
localhost:5173 的 localStorage:
  isLogin: "true"     ← Login 页面写入
                         ProtectRoute 读取 ← 同一个域名沙盒,共享

八、完整鉴权流程总结

把今天新增的所有零件串起来:

php 复制代码
用户点击"支付" → <Link to="/pay">
        │
        ▼
Routes 匹配 path="/pay"
        │
        ▼
<ProtectRoute> 被渲染
  localStorage.getItem('isLogin') === 'true' ?
        │
  ┌─────┴─────┐
  │ false     │ true
  ▼           ▼
重定向      渲染 <Pay />
/login
  │
state={{ from: '/pay' }}
  │
  ▼
Login 组件渲染
  useLocation().state?.from → '/pay'
  │
  ▼
用户输入 admin / 123456 → handleSubmit
  localStorage.setItem('isLogin', 'true')
  navigate('/pay', { replace: true })
  │
  ▼
ProtectRoute 再次检查
  isLogin === true → 放行
  │
  ▼
<Pay /> 正常渲染,URL 是 /pay

关键要点速查

概念 一句话解释
BrowserRouter 用 History API 操作完整 URL 路径,需要服务器 fallback 支持
HashRouter 用 URL # 部分做路由,零配置,URL 里有个 #
children 模式 组件标签内部的内容作为 prop 传入,实现松耦合的"插槽"
路由守卫 包裹敏感页面,检查权限后决定放行还是重定向
Navigatestate 跨页面传递"隐藏数据"(如来源路径),不进 URL,刷新不丢
useLocation() 读取浏览器当前 URL 的信息,包括 state
FormData 浏览器原生 API,从 <form> 提取带 name 属性的 input 值
localStorage 浏览器本地持久存储,按域名隔离
navigate(path, {replace}) 替换当前历史记录,不让用户回退到不该回的页面
Restful URL 路径对应资源,/products/123 就是 id=123 的商品

React Router 的本质能力可以归纳为三点:匹配 (URL → 组件)、导航 (组件 → URL)、传参(路由参数 + state)。鉴权路由则是这些基础能力的组合应用------检查状态(localStorage)、条件导航(Navigate)、跨页传参(state)、防回退(replace)。把这些原子能力理解透彻,你会发现任何复杂的前端路由场景,拆开来看都离不开这三个维度。

相关推荐
kyriewen1 小时前
我重写了自己用了两年的防抖节流Hook——发现里面藏着3个隐藏bug
前端·javascript·面试
阿祖zu1 小时前
芝士就是力量!开源私有化部署与 GitHub 双向同步的个人知识笔记 App
前端·后端·ios
漂流瓶jz1 小时前
Webpack开发环境:观察模式/webpack-dev-server/HMR热更新
前端·javascript·webpack
IT_陈寒1 小时前
SpringBoot自动配置失效?这个隐藏配置坑了我一整晚
前端·人工智能·后端
小爬的老粉丝2 小时前
Vue 3 文件预览生产排障:Worker/WASM 404、鉴权 Blob 与子路径
javascript·vue.js·wasm
凌涘2 小时前
前端路由(一):二十行代码实现 Hash 路由
前端
勾勾圈圈蛋蛋2 小时前
黑马Vue_day11:Vue3的watch,ref和reactive,vue2->vue3,defineOptions、Model
前端
AlloyTeamZy2 小时前
我发现,一个人做小游戏最难的,根本不是写代码
前端·人工智能·程序员
亲亲小宝宝鸭2 小时前
离谱架构系列——我在祖传代码里发现了【智子】
javascript