
做 SPA 项目路由是绕不过去的一关。页面切不动、白屏刷新、登录后跳不回原页面、DataCloneError报错…… 这些坑基本每个写 React 的人都踩过。这篇文章带你从零搭一套完整可用的路由系统懒加载、动态路由、嵌套路由、重定向、404 兜底、鉴权拦截全部实战代码复制就能跑。看完你能解决三件事搞懂前端路由到底在干什么配出一套生产可用的路由方案避开鉴权跳转的两个经典大坑一、为什么需要前端路由先说清楚一件事传统路由是后端的事。用户点个链接浏览器发请求后端返回新页面屏幕白一下体验拉胯。前后端分离后前端接管了页面切换这就是SPA单页应用。核心思路一句话URL 变了但页面不刷新由 JS 来切换显示的组件。React 生态里干这件事的就是react-router-dom。二、路由选型HashRouter 还是 BrowserRouter这俩都能用但原理不同。HashRouterURL 长这样http://localhost:5173/#/pay改的是 hash 部分#后面改 hash不会刷新页面监听hashchange事件就行缺点URL 有点丑BrowserRouterURL 长这样http://localhost:5173/pay走的是 HTML5 的historyAPIpushState/replaceStateURL 干净符合 RESTful 风格缺点部署时服务端要配置回退到 index.html否则刷新就 404本项目用的是BrowserRouterimport { 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 idcontainer 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 pathnew 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 pathnew 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别用aimport { Link } from react-router-dom; function Navigation() { return ( nav ul liLink to/Home/Link/li liLink to/aboutAbout/Link/li liLink to/user/123User/Link/li liLink to/products/123产品详情/Link/li liLink to/pay支付/Link/li /ul /nav ); }为什么不能用a因为a会触发整页刷新SPA 直接废了。Link内部调用history.pushState只改 URL 不刷新这才是 SPA 该有的样子。五、鉴权路由本文的重点这节是全文干货密度最高的地方两个坑我挨个讲。需求场景用户没登录就想访问/pay拦下来跳到登录页。登录成功后自动跳回他本来想去的/pay而不是傻乎乎地跳到首页。门禁组件ProtectRouteimport { 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 typetext nameusername placeholder请输入用户名 required / input typepassword namepassword placeholder请输入密码 required / button typesubmit登录/button /form ); };这行代码是整条链的灵魂constfromlocation.state?.from?.pathname||/;拆开看表达式含义location.state/login 路由上挂的 statelocation.state?.fromProtectRoute 传过来的原 location 对象?.from?.pathname取它的pathname即/pay?.可选链的作用直接访问 /login 时 state 是 undefined没有?.会直接报 TypeError。六、踩坑实录两个坑我替你踩过了坑 1DataCloneError: 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()浏览器用结构化克隆算法序列化它。而 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避坑原则传值和取值的数据结构必须严格对应跨组件传对象时尤其要确认字段名一致。坑 3FormData 取不到值这是个隐藏坑新手很容易中招。错误写法input typetext placeholder请输入用户名 required / input typepassword placeholder请输入密码 required /new FormData(form)只收集带name属性的表单控件。上面两个 input 没写nameformData.get(username)永远返回null登录永远失败。正确写法input typetext nameusername placeholder请输入用户名 required / input typepassword namepassword placeholder请输入密码 required /记住用 FormDatainput 必须有name这是 HTML 表单的基础规则跟 React 无关。七、路由对象速查表对象/Hook作用典型用法useNavigate代码里主动跳转navigate(/pay, { replace: true })useLocation拿当前路由信息location.pathname、location.stateuseParams取动态路由参数const { id } useParams()Link声明式跳转Link to/about关于/LinkNavigate组件式重定向Navigate to/login replace /Outlet嵌套路由占位父组件里渲染子路由navigate对应编程式导航Link对应声明式导航按场景选。八、总结一图看懂整套鉴权流程用户访问/payProtectRoute检查localStorage没登录Navigate跳/login把当前 location 塞进state.from登录页useLocation取出from.pathname登录成功navigate(from, { replace: true })跳回/pay三个核心原则路由 state 只放可序列化的普通对象别放 DOM 对象传值和取值的字段结构要严格对应表单控件必须加nameFormData 才能收集到值