news 2026/8/8 7:11:21

React Router 路由配置老是踩坑?7 种用法 + 3 个实战避坑,一次性讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React Router 路由配置老是踩坑?7 种用法 + 3 个实战避坑,一次性讲透

做 SPA 项目,路由是绕不过去的一关。

页面切不动、白屏刷新、登录后跳不回原页面、DataCloneError报错…… 这些坑,基本每个写 React 的人都踩过。

这篇文章带你从零搭一套完整可用的路由系统:懒加载、动态路由、嵌套路由、重定向、404 兜底、鉴权拦截,全部实战代码,复制就能跑。

看完你能解决三件事:

  • 搞懂前端路由到底在干什么
  • 配出一套生产可用的路由方案
  • 避开鉴权跳转的两个经典大坑

一、为什么需要前端路由

先说清楚一件事:传统路由是后端的事

用户点个链接,浏览器发请求,后端返回新页面,屏幕白一下,体验拉胯。

前后端分离后,前端接管了页面切换,这就是SPA(单页应用)

核心思路一句话:

URL 变了,但页面不刷新,由 JS 来切换显示的组件。

React 生态里,干这件事的就是react-router-dom


二、路由选型:HashRouter 还是 BrowserRouter

这俩都能用,但原理不同。

HashRouter

URL 长这样:http://localhost:5173/#/pay

  • 改的是 hash 部分(#后面)
  • 改 hash不会刷新页面,监听hashchange事件就行
  • 缺点:URL 有点丑

BrowserRouter

URL 长这样:http://localhost:5173/pay

  • 走的是 HTML5 的historyAPI(pushState/replaceState
  • URL 干净,符合 RESTful 风格
  • 缺点:部署时服务端要配置回退到 index.html,否则刷新就 404

本项目用的是BrowserRouter

import { BrowserRouter as Router } from 'react-router-dom'; <Router> {/* 应用内容 */} </Router>

新手建议:本地开发用 BrowserRouter 没问题,上线前记得配 nginx,不然刷新页面直接白屏。


三、路由配置实战:一套配齐 7 种用法

直接看核心配置文件,我把每种用法都标了注释:

import { lazy, Suspense } from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate, } from 'react-router-dom'; import Navigation from './component/Navigation'; import ProtectRoute from './ProtectRoute'; import Pay from './pages/Pay'; // 1. 路由懒加载:按需加载,提升首页速度 const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); const User = lazy(() => import('./pages/User')); const NotFound = lazy(() => import('./pages/NotFound')); const Products = lazy(() => import('./Products')); const ProductDetail = lazy(() => import('./Products/ProductDetail')); const NewProduct = lazy(() => import('./Products/New')); const Login = lazy(() => import('./pages/Login')); const App = () => { return ( <Router> <Suspense fallback={<div>等等我呗...</div>}> <Navigation /> <div id="container"> <Routes> {/* 基础路由 */} <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> {/* 2. 动态路由:冒号占位 */} <Route path="/user/:id" element={<User />} /> {/* 3. 嵌套路由:多级菜单 */} <Route path="/products" element={<Products />}> <Route path=":productId" element={<ProductDetail />} /> <Route path="new" element={<NewProduct />} /> </Route> {/* 4. 重定向:旧路径跳新路径 */} <Route path="/old-path" element={ <Navigate replace to="/products/new" /> } /> {/* 5. 鉴权路由:包裹一层门禁 */} <Route path="/login" element={<Login />} /> <Route path="/pay" element={ <ProtectRoute> <Pay /> </ProtectRoute> } /> {/* 6. 404 兜底:* 贪婪匹配所有未命中的路径 */} <Route path="*" element={<NotFound />} /> </Routes> </div> </Suspense> </Router> ); }; export default App;

逐个拆开说。

1. 懒加载:别让首页背全量包

lazy + Suspense是性能优化的标配。

没有懒加载,所有页面打包进一个 bundle,首页加载慢得像蜗牛。

用了懒加载,访问哪个页面才加载哪个 chunk,首页体积直接瘦下来。

Suspensefallback是加载时的占位,给用户一个"正在加载"的反馈。

2. 动态路由:一个参数搞定详情页

<Route path="/user/:id" element={<User />} />

/user/123/user/456共用同一个组件,组件里用useParams()取参数:

import { useParams } from 'react-router-dom'; const User = () => { const { id } = useParams(); return <div>用户ID:{id}</div>; };

详情页、编辑页都靠这一招。

3. 嵌套路由:父子结构清晰

产品模块下有列表、详情、新建,用嵌套路由最清晰:

<Route path="/products" element={<Products />}> <Route path=":productId" element={<ProductDetail />} /> <Route path="new" element={<NewProduct />} /> </Route>

父组件Products里用<Outlet />占位,子路由会渲染在占位处:

import { Outlet } from 'react-router-dom'; const Products = () => ( <div> <h1>产品列表</h1> <Outlet /> </div> );

4. 重定向:旧路径平滑迁移

<Route path="/old-path" element={<Navigate replace to="/products/new" />} />

replace表示替换历史记录,用户点后退不会回到旧路径。

5. 404 兜底

<Route path="*" element={<NotFound />} />

*是通配符,放在最后,前面没匹配上的全归它。


四、Link 组件:别用 a 标签

导航栏用Link,别用<a>

import { Link } from 'react-router-dom'; function Navigation() { return ( <nav> <ul> <li><Link to="/">Home</Link></li> <li><Link to="/about">About</Link></li> <li><Link to="/user/123">User</Link></li> <li><Link to="/products/123">产品详情</Link></li> <li><Link to="/pay">支付</Link></li> </ul> </nav> ); }

为什么不能用<a>

因为<a>会触发整页刷新,SPA 直接废了。

Link内部调用history.pushState,只改 URL 不刷新,这才是 SPA 该有的样子。


五、鉴权路由:本文的重点

这节是全文干货密度最高的地方,两个坑我挨个讲。

需求场景

用户没登录就想访问/pay?拦下来,跳到登录页。

登录成功后,自动跳回他本来想去的/pay,而不是傻乎乎地跳到首页。

门禁组件:ProtectRoute

import { Navigate, useLocation } from 'react-router-dom'; const ProtectRoute = ({ children }) => { const isLogin = localStorage.getItem('isLogin') === 'true'; const location = useLocation(); if (!isLogin) { // 把"从哪来"的信息塞进 state,登录页取出来跳回去 return <Navigate to="/login" replace state={{ from: location }} />; } return <div>{children}</div>; };

这里的关键是children

children是 React 的"插槽"机制——父组件包裹的子节点,会作为props.children传进来。

<ProtectRoute> <Pay /> {/* 这就是 children */} </ProtectRoute>

这种模式让ProtectRoute变成可复用的门禁:任何需要鉴权的页面,套一层就行,不用改原组件。

登录页:取值跳回

import { useNavigate, useLocation } from 'react-router-dom'; const Login = () => { const navigate = useNavigate(); const location = useLocation(); // 链式取值,拿不到就回退首页 const from = location.state?.from?.pathname || '/'; function handleSubmit(e) { e.preventDefault(); const formData = new FormData(e.currentTarget); const username = formData.get('username'); const password = formData.get('password'); if (username === 'admin' && password === '123456') { localStorage.setItem('isLogin', 'true'); navigate(from, { replace: true }); } else { alert('登录失败'); } } return ( <form onSubmit={handleSubmit}> <h1>登录页</h1> <input type="text" name="username" placeholder="请输入用户名" required /> <input type="password" name="password" placeholder="请输入密码" required /> <button type="submit">登录</button> </form> ); };

这行代码是整条链的灵魂

constfrom=location.state?.from?.pathname||'/';

拆开看:

表达式含义
location.state/login 路由上挂的 state
location.state?.fromProtectRoute 传过来的原 location 对象
?.from?.pathname取它的pathname,即/pay
`

?.可选链的作用:直接访问 /login 时 state 是 undefined,没有?.会直接报 TypeError


六、踩坑实录:两个坑我替你踩过了

坑 1:DataCloneError: Location object could not be cloned

报错现场

Uncaught DataCloneError: Failed to execute 'replaceState' on 'History': Location object could not be cloned.

错误写法

// 直接用了全局 window.location,它是 DOM 对象 return <Navigate to="/login" replace state={{ from: location }} />

这里的locationwindow.location,是浏览器宿主对象。

React Router 内部会把state传给history.replaceState(),浏览器用结构化克隆算法序列化它。而 DOMLocation对象无法被克隆,于是抛错。

正确写法:用useLocation()拿到的是普通对象,可克隆:

const location = useLocation(); return <Navigate to="/login" replace state={{ from: location }} />

记住一句话:路由相关的东西,永远用react-router-dom提供的 hook,别碰window.location

坑 2:登录后回不到原页面,跳到了首页

错误现场:从/pay被拦到/login,登录成功后却跳到了/

原因:两边数据格式对不上。

// ProtectRoute 传的是字符串 state={{ from: location.href }} // "http://localhost:5173/pay" // Login 却按对象取 location.state?.from?.pathname // 字符串上没有 pathname,结果是 undefined

字符串上没有.pathname,取出来是undefined|| '/'兜底生效,于是跳到了首页。

修复:两边统一成对象形态。

// ProtectRoute state={{ from: location }} // 传 useLocation() 的对象 // Login location.state?.from?.pathname // 正确取到 "/pay"

避坑原则:传值和取值的数据结构必须严格对应,跨组件传对象时尤其要确认字段名一致。

坑 3:FormData 取不到值

这是个隐藏坑,新手很容易中招。

错误写法

<input type="text" placeholder="请输入用户名" required /> <input type="password" placeholder="请输入密码" required />

new FormData(form)只收集name属性的表单控件。上面两个 input 没写nameformData.get('username')永远返回null,登录永远失败。

正确写法

<input type="text" name="username" placeholder="请输入用户名" required /> <input type="password" name="password" placeholder="请输入密码" required />

记住:用 FormData,input 必须有name,这是 HTML 表单的基础规则,跟 React 无关。


七、路由对象速查表

对象/Hook作用典型用法
useNavigate代码里主动跳转navigate('/pay', { replace: true })
useLocation拿当前路由信息location.pathnamelocation.state
useParams取动态路由参数const { id } = useParams()
Link声明式跳转<Link to="/about">关于</Link>
Navigate组件式重定向<Navigate to="/login" replace />
Outlet嵌套路由占位父组件里渲染子路由

navigate对应编程式导航,Link对应声明式导航,按场景选。


八、总结

一图看懂整套鉴权流程:

  1. 用户访问/pay
  2. ProtectRoute检查localStorage,没登录
  3. Navigate/login,把当前 location 塞进state.from
  4. 登录页useLocation取出from.pathname
  5. 登录成功,navigate(from, { replace: true })跳回/pay

三个核心原则

  • 路由 state 只放可序列化的普通对象,别放 DOM 对象
  • 传值和取值的字段结构要严格对应
  • 表单控件必须加name,FormData 才能收集到值
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/8 7:11:16

ITIL 4实践选择三步走策略:评估、匹配与验证

1. ITIL 4实践选择的三步走策略解析ITIL 4作为当前IT服务管理领域最前沿的框架&#xff0c;其34个管理实践让许多企业在落地时面临选择困难。经过多个大型企业项目的实战验证&#xff0c;我总结出一套"评估-匹配-验证"的三步走策略&#xff0c;能帮助企业从茫然状态快…

作者头像 李华
网站建设 2026/8/8 7:07:58

Linux进程间通信:消息队列与信号量的原理、实战与优化

1. 从“单打独斗”到“协同作战”&#xff1a;为什么需要进程间通信&#xff1f;在Linux的世界里&#xff0c;每个进程都像一座孤岛&#xff0c;拥有自己独立的地址空间。这确保了安全与稳定&#xff0c;一个进程的崩溃不会轻易拖垮整个系统。但现实中的任务往往是复杂的&#…

作者头像 李华
网站建设 2026/8/8 7:04:26

线程池队列堆积:原因、监控与解决方案

前言 线程池队列从几十涨到几千&#xff0c;通常不是“队列参数太小”&#xff0c;而是任务进入速度已经超过完成速度。队列只是把差额暂时存了下来。 很多处理方式会让问题变得更隐蔽&#xff1a;把队列从1000改成10000&#xff0c;告警暂时消失&#xff0c;但排在后面的任务要…

作者头像 李华
网站建设 2026/8/8 7:03:23

数据结构-环形链表

单向环形链表头结点创建动态申请内存创建循环链表哨兵头结点&#xff1b;内存分配失败&#xff0c;打印提示并返回 NULL&#xff1b;将头结点 next 指针指向自身&#xff0c;构造空循环链表&#xff1b;返回头结点地址。node_t *cycle_linklist_create(void) {node_t *head ma…

作者头像 李华
网站建设 2026/8/8 6:59:32

基于Web UI与单片机的Wi-Fi握手包捕获与安全测试实践

这次我们来看一个围绕“抓取握手包”和“Wi-Fi密码破解”的本地化、低门槛实现方案。这个项目的核心不是教你如何破解他人网络&#xff0c;而是提供一个用于安全研究、渗透测试教学和无线网络协议分析的Web UI工具。它特别强调了在嵌入式设备&#xff08;如基于BW16芯片的单片机…

作者头像 李华