云计算百科
云计算领域专业知识百科平台

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 的 history API(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,首页体积直接瘦下来。

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

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>
);
};

这行代码是整条链的灵魂

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

拆开看:

表达式含义
location.state /login 路由上挂的 state
location.state?.from ProtectRoute 传过来的原 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 }} />

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

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

正确写法:用 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 没写 name,formData.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.pathname、location.state
useParams 取动态路由参数 const { id } = useParams()
Link 声明式跳转 <Link to="/about">关于</Link>
Navigate 组件式重定向 <Navigate to="/login" replace />
Outlet 嵌套路由占位 父组件里渲染子路由

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


八、总结

一图看懂整套鉴权流程:

  • 用户访问 /pay
  • ProtectRoute 检查 localStorage,没登录
  • Navigate 跳 /login,把当前 location 塞进 state.from
  • 登录页 useLocation 取出 from.pathname
  • 登录成功,navigate(from, { replace: true }) 跳回 /pay
  • 三个核心原则:

    • 路由 state 只放可序列化的普通对象,别放 DOM 对象
    • 传值和取值的字段结构要严格对应
    • 表单控件必须加 name,FormData 才能收集到值
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » React Router 路由配置老是踩坑?7 种用法 + 3 个实战避坑,一次性讲透
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!