
做 React 开发这些年几乎每个项目都离不开路由。React Router 这个库刚开始接触时觉得很简单不就是配置几个 Route 把页面串起来吗等真正做起项目才发现这里面的门道比想象中多得多嵌套路由怎么组织、刷新页面 404 怎么处理、登录鉴权怎么拦截、路由变化怎么监听、面包屑怎么根据路由生成……每一个单拎出来都是实际开发躲不开的问题。这篇文章我会从零开始把我从入门到在多个项目中落地 React Router 的经验完整梳理一遍。我不会只讲 API而是按照真实项目开发的顺序来写先讲清楚路由到底解决了什么问题再从配置、传参、嵌套布局、鉴权一步步往下走最后把常见的坑和排查思路列成清单。无论你是刚接触 React 的初学者还是被路由问题折腾过的中级开发者这篇文章应该都能帮你省下不少查资料的功夫。版本说明一下我用的所有代码示例都以 React Router v6 为准。现在是 2024 年了v6 早就是稳定版本网上很多教程还是 v5 的老写法直接拿过来用会出现各种兼容问题这一块后面我会专门列一节讲区别。1. 为什么需要路由先搞懂它解决什么问题1.1 没有路由的世界SPA 的困境先说个最基础的问题React 本身是构建单页应用SPA的框架单页应用最大的特点就是只有一个 HTML 页面所有内容都是 JavaScript 动态渲染的。如果没有路由你面对的会是一个极其分裂的开发状态切换页面只能靠组件内部的 state 控制比如if (page home)显示首页if (page about)显示关于页。这种方式在只有两三个页面、且不需要用户通过 URL 直达某个页面时勉强能用。但一旦项目超过五个页面麻烦就来了用户刷新一下浏览器就回到了默认页浏览器前进后退按钮完全失效你没法把一个页面链接发给同事让他直接打开后端也无法区分用户当前在做什么操作。可以说SPA 缺了路由就像一个没有门牌号和走廊的大楼屋子再多也没法有条理地进出。React Router 就是这个“门牌号和走廊”的解决方案。它本质上做的事情有两件一是把 URL 和 UI 组件绑定起来让浏览器地址栏的变化能够驱动页面内容的切换二是提供一套导航机制让用户通过点击链接、按钮或手动改地址都能到达对应的页面。1.2 React Router 的核心设计思想React Router v6 的设计思路一句话可以概括路由即组件。在 v6 里你声明路由的方式不是写配置对象而是直接写 JSX 组件树。比如BrowserRouter包在最外层里面用Routes和Route来声明路径与组件的映射关系。这种设计的好处在于它完全融入了 React 的声明式编程模型。路由不是脱离于组件之外的特殊配置而是组件树的一部分。这意味着你可以像组合普通组件一样组合路由可以在路由组件外面包一层自定义组件来做权限校验也可以利用 React Context 在路由内部共享数据。另外一个重要设计是“嵌套路由”成为一等公民。v6 的路由结构是完整的树形结构父路由通过Outlet /这个占位组件来渲染子路由的内容。这个设计直接解决了后台管理系统最经典的布局问题整个系统共用一个侧边栏和顶栏只有中间内容区域随 URL 变化。Route path/admin element{AdminLayout /} Route pathdashboard element{Dashboard /} / Route pathusers element{Users /} / /Route上面这段代码里访问/admin/dashboard时AdminLayout组件会被渲染它的侧边栏和顶栏会保留而Outlet /的位置会渲染Dashboard组件。这种“父组件框架 子组件内容”的模式在 v5 时代实现起来非常别扭v6 算是彻底解决了。2. 环境准备与基础路由配置从零跑通第一个例子2.1 安装与最小示例安装 React Router 只需要一条命令。我用的是 npm你用 yarn 或 pnpm 也是一样npm install react-router-dom6注意一点React Router 在 npm 上有两个相关的包react-router和react-router-dom。前者是核心库后者是专门为浏览器 DOM 环境封装的一层包含了我们常用的BrowserRouter、Link等组件。实际做 Web 开发直接装react-router-dom就行它会自动带上react-router作为依赖。装完之后最基础的使用方式是在入口文件里用BrowserRouter把应用包起来import { BrowserRouter } from react-router-dom; function App() { return ( BrowserRouter MainApp / /BrowserRouter ); }然后在你需要定义路由的地方用Routes和Route声明路径映射import { Routes, Route } from react-router-dom; function MainApp() { return ( Routes Route path/ element{HomePage /} / Route path/about element{AboutPage /} / Route path/contact element{ContactPage /} / /Routes ); }这个示例很简单但我第一次实际写的时候踩了一个小坑Routes组件是 v6 才有的v5 里对应的组件叫Switch功能类似但不完全相同。如果你在网上找到的教程里用的是Switch那基本可以确定是 v5 甚至更早的文章照着写会直接报错说Switch is not defined或Switch不存在。另外v6 里Route不能再直接写子组件作为内容必须用element属性传组件元素这是很多人从 v5 迁移过来最容易改错的地方。2.2 页面跳转Link 与 NavLink路由配置好了接下来就是页面之间的跳转方式。React 里做跳转有两个选择一是用Link或NavLink组件本质上是渲染一个a标签但拦截了浏览器默认的整页刷新行为改为通过 history API 切换 URL 并触发路由更新二是用编程式导航useNavigate在事件回调或某个异步操作完成后触发跳转。import { Link, NavLink } from react-router-dom; function Header() { return ( nav Link to/home首页/Link NavLink to/about className{({ isActive }) (isActive ? active-link : )} 关于 /NavLink /nav ); }NavLink和Link的区别在于它会在当前路由匹配时自动加上一个激活状态。上面代码里我通过className传入的函数拿到isActive动态绑定样式。这个功能在做导航菜单高亮时特别常用。不过这里有一个容易踩的坑NavLink默认是“包含匹配”。比如/about这个 NavLink当你在/about/team这个子路径时它依然会被标记为激活。如果你只希望完全匹配时才高亮需要加end属性。我第一次做后台导航时就因为这个首页导航一直高亮着排查了半天才发现是匹配规则的问题。2.3 默认路由与 404 兜底配置路由时有两个几乎每次都要处理的情况默认重定向和 404 页面。v6 里实现方式如下Routes Route path/ element{Navigate to/home replace /} / Route path/home element{HomePage /} / Route path/about element{AboutPage /} / Route path* element{NotFoundPage /} / /Routes当用户访问/时Navigate组件会帮我们自动跳转到/homereplace属性表示替换当前历史记录这样用户按返回键不会回到一个空白根路径。path*是通配符匹配所有未定义的路由用来渲染 404 页面。提示实际项目中 404 页面最好不只是显示“页面不存在”这几个字可以加一个返回首页的按钮并且最好带上“检查地址是否输入正确”之类的提示文案能明显降低用户的困惑感。3. 嵌套路由与布局系统后台管理的核心玩法3.1 Outlet 的作用与嵌套路由声明方式后台管理系统是 React Router 嵌套路由最典型的应用场景。想象一个管理后台的常见布局左侧是菜单栏顶部是用户信息栏中间是内容区域。如果用传统的组件状态控制页面切换光菜单栏的选中状态就够维护半天。嵌套路由可以直接把整个布局结构固化下来。声明嵌套路由有两种方式。第一种是在父Route里直接写子RouteRoutes Route path/admin element{AdminLayout /} Route index element{Dashboard /} / Route pathusers element{Users /} / Route pathsettings element{Settings /} / /Route /Routes第二种是把子路由抽取出来定义再通过属性组合。两种方式效果一样我习惯用第一种因为路由结构一眼就能看全。关键点在于AdminLayout组件里面必须有一个Outlet /占位function AdminLayout() { return ( div classNameadmin-layout Sidebar / div classNameadmin-content Outlet / /div /div ); }访问/admin/users时AdminLayout渲染Sidebar正常显示而内容区域的位置由Outlet /接管React Router 会在这一行渲染匹配到的Users组件。这就是嵌套路由的魔力父路由组件负责整体布局子路由组件负责具体内容。3.2 使用 index 路由设置默认子页面上面代码里我写了一个Route index element{Dashboard /} /这个index路由也很关键。它表示当 URL 恰好匹配父路由的路径即/admin时默认渲染哪个子组件。没有这个index路由你访问/admin时AdminLayout会渲染但Outlet /的位置是空的页面上只有侧边栏中间一片空白。很多新手第一次写嵌套路由都会遇到“为什么父路由的子区域是空白的”这种问题十有八九就是忘了写 index 路由。还有一个细节嵌套子路由的路径不需要写完整路径。比如/admin/users里子路由只需要写users而不是/admin/users。如果你在子路由的 path 前面多写了/React Router 会把它当成根路径下的绝对路由去匹配结果就是访问/admin/users时匹配不到任何子路由页面照样空白。这个坑我帮同事排查过好几次。3.3 多个 Outlet实现动态渲染区域的进阶用法一个组件里可以放多个Outlet不过需要给它们起名字。这在做“文章详情页里的相关推荐”或者“弹窗类内容需要独立于主内容区渲染”之类场景时非常有用React Router v6 通过outlet这个上下文机制来区分Route path/detail/:id element{ArticleLayout /} Route pathcontent element{ArticleContent /} / Route pathrecommend element{Recommend /} outletsidebar / /Route对应布局组件里function ArticleLayout() { return ( div {/* 主内容区 */} Outlet / {/* 侧边栏区域专门渲染带 sidebar 标识的子路由 */} Outlet namesidebar / /div ); }说实话多个 Outlet 的功能我在实际项目中用到的次数不多但一旦遇到就会觉得它特别好用。比如文章页的结构左侧是正文内容右侧是“你可能会喜欢”的推荐列表推荐列表的数据和布局跟正文完全独立就可以用带名字的 Outlet 来做。每个 Outlet 相当于给子路由内容划分了一个“插槽”这不只是布局上的便利还能让路由层直接控制不同区域的展示逻辑减少组件里的条件判断。4. 路由传参与导航从 URL 到组件数据的完整通道4.1 三种传参方式对比实际开发中几乎没有哪个页面是不需要传参的列表页跳详情页要传 id搜索结果要传关键词表单提交后要回跳到来源页……React Router 提供了多种传参方式各自适合不同场景。首先是用useParams读取路径参数。这种方式适合“资源的唯一标识”比如文章详情的 id、用户信息的 userId。定义路由时在 path 里用冒号声明参数名Route path/article/:id element{ArticleDetail /} /在组件中读取import { useParams } from react-router-dom; function ArticleDetail() { const { id } useParams(); // 用 id 去请求文章数据 }第二种是 URL 查询参数query string也就是地址栏里?page2keywordreact这种形式。用useSearchParams读取和修改import { useSearchParams } from react-router-dom; function SearchPage() { const [searchParams, setSearchParams] useSearchParams(); const keyword searchParams.get(keyword) || ; const handleSearch (value) { setSearchParams({ keyword: value, page: 1 }); }; }第三种是通过 state 传递数据。这种方式不会把数据暴露在 URL 上适合传递“临时性、非关键”的数据比如从列表页跳转编辑页时先把这一行的对象数据通过 state 带过去减少一次查询const navigate useNavigate(); navigate(/user/edit, { state: { user: rowData } }); // 在编辑页读取 const location useLocation(); const user location.state?.user;提示通过 state 传的数据在刷新页面后会丢失因为刷新的本质是重新加载整个页面浏览器的历史记录里不会保存这个 state。所以如果数据是必须的刷新后不能丢一定要通过 URL 参数传或者从后端接口重新获取。4.2 编程式导航useNavigate 的完整用法Link组件适用于纯粹的点击跳转。但有时候跳转不是用户直接点击触发的比如登录成功后自动跳到之前想访问的页面提交表单成功后跳到列表页倒计时结束后跳到抽奖页……这些场景都需要用编程式导航。import { useNavigate } from react-router-dom; function LoginButton() { const navigate useNavigate(); const handleLogin () { // 模拟登录成功 const redirectUrl location.state?.from || /home; navigate(redirectUrl, { replace: true }); }; return button onClick{handleLogin}登录/button; }useNavigate返回一个函数调用时接受两个参数目标路径和配置对象。配置对象里常用的有两个字段replace表示替换当前历史记录这样用户在登录成功后按返回键不会回到登录页循环state可以携带数据跟前面讲的Link的 state 传参是同一个机制。另外navigate还可以传入一个数字比如navigate(-1)表示返回上一页navigate(1)表示前进一页。这个在实现“返回列表”按钮时很实用但要注意如果上一页已经是不存在的历史记录比如用户直接输入 URL 打开的页面navigate(-1)会直接跳到一个空白历史页。所以稳妥的写法是先判断一下window.history.length或者在跳转时通过location.key判断有没有上一条记录。4.3 路由匹配的完整规则path 写法大全v6 的路由匹配规则比 v5 更严格也更直观但有一些细节不仔细看文档容易弄错。我把常用的 path 写法整理成一个表格写法匹配的 URL 示例说明//只匹配根路径about/about相对路径需在父路由内使用article/:id/article/123路径参数用 useParams 读取article/:id?/article或/article/123参数可选v6.5 之后支持files/*/files/a.jpg、/files/src/index.js通配符匹配任意多级路径*任意路径通常用于 404/admin/:id(\d)/admin/123不匹配/admin/abc正则约束参数格式正则约束是 v6 比较实用的一个特性。比如我的后台项目里文章 id 是纯数字我就在路由上直接约束Route path/article/:id(\d) element{ArticleDetail /} /这样用户访问/article/abc时根本不会匹配到详情页组件会直接落到 404 上省去了在组件里判断 id 是否为数字的代码。不过要注意正则写法在 v6 里有调整不同小版本行为略有差异生产环境用之前最好在自己项目的 React Router 版本上实测一下。5. 路由守卫与鉴权给每个页面加上访问门槛5.1 用包装组件实现登录拦截绝大多数后台管理系统都有权限需求未登录用户访问任何后台页面都要跳到登录页已登录但没有某个角色权限的用户访问特定页面时应该看到无权限提示。React Router v6 没有专门的“路由守卫”API实现方式是用包装组件这个思路其实比配置式的守卫更贴合 React 组件化思维。核心代码是这样function RequireAuth({ children }) { const token localStorage.getItem(token); const location useLocation(); if (!token) { // 把当前要访问的路径保存下来登录成功后再跳回来 return Navigate to/login state{{ from: location.pathname }} replace /; } return children; }然后在路由声明时把需要鉴权的路由包进去Routes Route path/login element{Login /} / Route path/ element{ RequireAuth AdminLayout / /RequireAuth } Route index element{Dashboard /} / Route pathusers element{Users /} / /Route /Routes这样做的效果是访问/下的任何子路由时RequireAuth都会先在组件树顶层检查一次 token没有 token 直接Navigate到登录页。相比在每一个页面组件内部各自判断这种写法把鉴权逻辑收敛到了一个地方管理和修改都方便得多。5.2 按角色控制菜单和权限路由只做登录拦截在真实项目里还不够。系统通常有管理员、编辑、访客等多种角色不同角色能看到的菜单和页面不一样。我的做法是用后端返回的权限码列表动态过滤菜单再在路由层加一层角色判断。权限校验组件function RequirePermission({ permission, children }) { const { permissions } useAuth(); if (!permissions.includes(permission)) { return Result status403 title403 subTitle抱歉您没有权限访问该页面 /; } return children; }在路由中的用法Route pathusers element{ RequirePermission permissionuser.manage Users / /RequirePermission } /菜单部分则根据权限列表做条件渲染const menuConfig [ { path: /dashboard, label: 工作台, permission: dashboard.view }, { path: /users, label: 用户管理, permission: user.manage }, { path: /settings, label: 系统设置, permission: system.setting }, ]; const visibleMenus menuConfig.filter((item) permissions.includes(item.permission));这套方案的核心思路是菜单是动态生成的路由是静态声明的权限校验是在进入页面时做的。三者各司其职菜单只是入口的展示层真正决定你能不能访问一个页面是路由层的RequirePermission。这样做避免了“菜单藏起来但手输 URL 还是能进”这种低级漏洞。我见过一些项目为了省事只做了菜单过滤结果用户在地址栏直接输入/users就顺畅地进入了管理页面这属于比较严重的权限漏洞路由层的拦截一定不能省。5.3 登录后回跳与防止循环跳转搭配鉴权组件有一个同样重要的小逻辑登录成功后的回跳。前面RequireAuth里我把未登录用户想访问的路径存到了state的from字段。登录成功后应该读取这个字段如果存在就跳回原页面否则跳默认首页const location useLocation(); const navigate useNavigate(); const handleLogin () { // 登录验证逻辑... const from location.state?.from || /home; navigate(from, { replace: true }); };这里有几个容易踩的坑。第一个是循环跳转如果用户直接访问/login登录后from是/login跳回去又是登录页就形成了死循环。所以跳转前最好判断一下from ! /login。第二个是from可能包含 query 参数比如用户访问的是/admin?tabusers如果只存location.pathname登录后 query 参数就丢了。更稳妥的做法是存location.pathname location.search。if (!token) { return ( Navigate to/login state{{ from: ${location.pathname}${location.search} }} replace / ); }这个细节看起来不起眼但实际项目中真的遇到过用户没登录点了一个带筛选条件的列表页链接登录后跳回来筛选条件全部丢失还得重新设置。存 query 参数的方案就避免了这个问题。6. 懒加载与代码分割让路由不再拖慢首屏6.1 React.lazy 配合 Suspense单页应用有个天然的问题打包出来的 JavaScript 文件会随着页面数量增加而膨胀。只要几十个页面打包文件轻松突破 1MB用户打开首页就得下载全部页面的代码网络稍慢就是白屏几秒钟。解决思路是代码分割把每个路由页面拆分成独立的 chunk用户访问哪个路由才下载哪个页面的代码。React Router v6 配合 React 的lazy和Suspense做这事非常顺手import { lazy, Suspense } from react; import { Routes, Route } from react-router-dom; const HomePage lazy(() import(./pages/HomePage)); const AboutPage lazy(() import(./pages/AboutPage)); const ArticleDetail lazy(() import(./pages/ArticleDetail)); function App() { return ( Suspense fallback{LoadingPage /} Routes Route path/ element{HomePage /} / Route path/about element{AboutPage /} / Route path/article/:id element{ArticleDetail /} / /Routes /Suspense ); }核心原理就一行lazy(() import(./pages/HomePage))。import()是动态导入语法打包工具Webpack 或 Vite看到这个语法会自动把HomePage单独打成一个小 chunk。Suspense的作用是在这个 chunk 还在下载时显示一个 fallback避免页面直接白屏。6.2 按路由拆分的实践配置实际项目中路由非常多我不会把每个页面都手写一遍lazy那样太啰嗦。我一般会抽一层公共的路由懒加载封装import { lazy } from react; function lazyLoad(path) { return lazy(() import(../pages/${path})); } // 路由配置时直接使用 const HomePage lazyLoad(HomePage); const AboutPage lazyLoad(AboutPage); const NotFoundPage lazyLoad(NotFoundPage);不过这种动态拼接路径的写法要小心import()的动态参数必须是静态可分析的路径前缀打包工具才能找到目标模块。也就是说../pages/这个前缀必须是字面量不能从变量里拼出整个路径。上面例子里path变量只影响文件名部分前缀是固定的这是正确的写法。如果整个路径都是变量Webpack 会编译不过或者打出一个非常大但内容不明确的 chunk。还有一点要提醒开发环境里因为本地编译快、网络开销小懒加载的优势不明显但到了生产环境差别非常大。我做过一次优化把 30 多个页面的后台系统从全部打到一个大包里拆成按路由懒加载首屏加载时间从原来的 3.4 秒降到了 1.2 秒左右这个优化性价比极高强烈建议每个 React 项目都做。7. 常见问题与排查技巧实录7.1 部署后刷新页面 404 的问题这是 React Router 项目上线后碰到的最高频问题本地开发一切正常部署到服务器后从首页点进子页面正常但一旦在子页面刷新就出现 404 或者“无法访问”。问题的根源在于开发环境下vite 或 webpack-dev-server 内置了 history fallback也就是当你请求一个不存在的路径时开发服务器会自动返回 index.html 给前端路由去处理。但生产环境的静态服务器Nginx、Tomcat、Apache 等默认没有这个配置它会在磁盘上找一个和 URL 路径对应的真实文件找不到就返回 404。解决方案是在服务器层面做 try_files 配置。以 Nginx 为例location / { try_files $uri $uri/ /index.html; }这一段配置的意思是先尝试找与请求路径匹配的真实文件找不到就回退到/index.html由前端路由接管。这是部署层面最标准的方案。如果项目部署在子路径下比如访问地址是https://example.com/my-app/那还需要做两件事一是BrowserRouter要传入basename属性BrowserRouter basename/my-app {/* 路由配置 */} /BrowserRouter二是 Nginx 的 try_files 要放在子路径对应的 location 里。这个坑我踩过一次basename 漏了页面跳转后所有资源全部 404因为脚本和样式的绝对路径指向了域名根目录找不到my-app下的资源。7.2 路由变化后页面位置没有回顶部单页应用的另一个常见体验问题是用户在一个很长的列表页往下滚了很久点击进入详情页再返回列表页时页面停留在他之前滚动的位置而不是从顶部开始。这在某些场景下是有用的比如列表页想恢复之前的位置但实际上大多数用户的预期是进入新页面时回到顶部。最简单的解决方案是写一个滚动恢复组件挂在路由出口的位置import { useEffect } from react; import { useLocation } from react-router-dom; function ScrollToTop() { const { pathname } useLocation(); useEffect(() { window.scrollTo(0, 0); }, [pathname]); return null; } // 放在 Routes 外面或里面都行 function App() { return ( BrowserRouter ScrollToTop / Routes {/* 路由声明 */} /Routes /BrowserRouter ); }它的原理很简单监听pathname变化一旦路由地址改变就把滚动条拉回顶部。当然如果项目里有部分页面希望保留滚动位置那就不能一刀切。更精细的做法是在组件内通过useEffect控制自己的滚动行为或者给ScrollToTop加一个配置项排除某些路径。我目前的经验是大部分项目一刀切回顶部就够了如果需要保留位置、再对特定页面做特殊处理这样最简单不容易产生 bug。7.3 如何在组件外部获取路由能力React Router 的 Hook 只能在组件函数里使用这是 React 的规则。但实际编码中经常会遇到一个场景在封装的 axios 请求工具里接口返回 401 时需要跳转到登录页。而 axios 拦截器不是组件没法直接调用useNavigate。这个问题的常规解决方案是创建一个路由跳转的工具模块import { createBrowserRouter } from react-router-dom; export const router createBrowserRouter(routes); // 在任意地方都能使用 export function navigateTo(path) { router.navigate(path); }然后在 axios 拦截器里调用import { navigateTo } from /utils/router; // 响应拦截器 instance.interceptors.response.use( (response) response, (error) { if (error.response?.status 401) { navigateTo(/login); } return Promise.reject(error); } );这里我用的是 v6.4 之后提供的createBrowserRouter创建路由实例的方式。这种创建方式返回的 router 对象暴露了navigate方法可以让我们在任意 JS 模块中触发路由跳转。如果项目用的是传统的BrowserRouter声明式方式没有向外暴露 router 实例那可以把跳转逻辑抽到事件里面或者用一个全局变量手动保存 navigate 引用// navigateHelper.js let navigateFn null; export function setNavigate(fn) { navigateFn fn; } export function navigateTo(path) { if (navigateFn) navigateFn(path); } // 在某个组件里注册 function App() { const navigate useNavigate(); useEffect(() { setNavigate(navigate); }, [navigate]); // ... }两种方案我都在项目里用过前者更干净后者在不改动整体架构的前提下也能解决问题。没有绝对的好坏看项目现状。7.4 版本差异从 v5 迁移到 v6 的核心变更点React Router v6 相比 v5 改动非常大。如果你接手的是老项目需要升级或者看老教程写新代码下面几个点必须留意Switch改成了Routes。Switch在 v6 里直接移除了没有兼容层只能全量替换。Route承载组件的方式从component属性改成了element属性。Route path/ component{Home} /要写成Route path/ element{Home /} /。路由嵌套从“组件内部写子路由”改成了“直接写在 Route 子树里”。v5 里你必须在子组件里再写一个Switchv6 直接父子嵌套即可配合Outlet /使用。useHistory改名为useNavigate返回的不再是 history 对象而是一个函数。v6 的路由匹配采用自动打分排序不再有 v5 里“越靠前优先级越高”的说法。这也意味着你不需要手动给路由排顺序通配符和静态路径之间怎么匹配React Router 有自己的规则大多数时候表现是合理的。Link组件基本没变但NavLink的激活类名判断逻辑变了需要用函数模式的className或style来拿到isActive状态。我做过的 v5 到 v6 迁移项目最大的坑在于嵌套路由。v5 时代很多项目用“父路由匹配布局 子路由在组件内部再定义”的方式升级时需要把子路由全部提升到父路由的声明里并且用Outlet /替换原本子路由渲染位改动面比较大。如果项目页面多、层层嵌套深建议用 codemod 工具先做一遍自动替换再手工处理特殊场景。7.5 一个容易被忽略的性能细节不要在组件内部重复渲染路由配置我在不少项目里见过这样的写法把Routes和Route直接放在某个组件的 render 里而这个组件在页面切换时频繁重新渲染。如果路由配置对象在每次渲染时都是新建的比如直接写 JSXReact Router 每次都得重新处理整个路由树页面切换会有可感知的卡顿。改进方式很简单把Routes的配置放在组件外部用常量保存或者用useMemo缓存。使用createBrowserRouter集中创建路由实例这也更有利于配合懒加载和数据加载 API 使用。// 路由配置独立成文件 export const routes [ { path: /, element: Navigate to/home replace / }, { path: /home, element: HomePage / }, { path: /about, element: AboutPage / }, ]; // 组件内稳定引用 import { routes } from /routes; function App() { return ( BrowserRouter Routes {routes.map((route) ( Route key{route.path} {...route} / ))} /Routes /BrowserRouter ); }这个优化不到十行代码但对大型项目的路由性能帮助很明显。我在一个四十多个路由的项目里实测过把动态渲染的路由改成static 配置后页面切换的响应时间从原先偶尔卡顿变成一直流畅虽然没有精确到毫秒级的数据统计但感知上的差异是实实在在的。8. 实战项目中的完整路由结构示例写了这么多单点内容最后还是放一个我在真实后台项目中用过的路由结构模板综合了嵌套布局、懒加载、权限控制、默认路由和 404。你可以把它当作一个可以直接参考的骨架在此基础上按自己的业务调整。// router.jsx import { createBrowserRouter, Navigate } from react-router-dom; import { lazy, Suspense } from react; const Login lazy(() import(./pages/Login)); const AdminLayout lazy(() import(./layouts/AdminLayout)); const Dashboard lazy(() import(./pages/Dashboard)); const Users lazy(() import(./pages/Users)); const Settings lazy(() import(./pages/Settings)); const NotFound lazy(() import(./pages/NotFound)); const router createBrowserRouter( [ { path: /login, element: Login /, }, { path: /, element: ( RequireAuth AdminLayout / /RequireAuth ), children: [ { index: true, element: Navigate to/dashboard replace / }, { path: dashboard, element: Dashboard / }, { path: users, element: ( RequirePermission permissionuser.manage Users / /RequirePermission ), }, { path: settings, element: Settings / }, ], }, { path: *, element: NotFound /, }, ], { basename: /my-app, } ); export default router;入口文件// main.jsx import { RouterProvider } from react-router-dom; import router from ./router; function App() { return ( Suspense fallback{div classNamepage-loading页面加载中.../div} RouterProvider router{router} / /Suspense ); }createBrowserRouter返回的 router 实例直接传给RouterProvider不需要再手动包一层BrowserRouter。这种写法在 v6.4 之后是官方推荐的方式尤其适合需要路由级数据预加载和权限控制的场景。组件内的跳转依然可以用useNavigate没有任何使用差异而且因为 router 对象是模块级导出的前面说的“在 axios 拦截器里跳转”也顺手就解决了。这个结构里还有一个小地方值得注意我把RequireAuth放在AdminLayout外层而RequirePermission放在具体的页面组件外层。前者的作用是“没登录就不能进后台体系”后者是“登录了但没权限就不给看某个页面”。两个鉴权职责是不一样的分开放比全部堆在一层要清晰得多后面维护时你只需要按职责定位问题。我个人在实际项目里的体会是React Router 的上手门槛真的不高半天时间看一遍官方文档就能写出能跑的配置。但真正决定一个应用路由层质量高低的是这些细节有没有做好嵌套布局抽象、有没有统一的权限拦截方案、有没有考虑部署后的刷新问题、有没有做代码分割优化首屏。这些恰恰是文档里不会手把手教的也是我在多个项目里反复踩坑总结出来的。最后再分享一个建议不管项目大小都值得花一点时间把路由设计成一个独立模块统一出口统一鉴权统一懒加载。这个设计做在前面后面每增加一个页面成本就只是一行路由声明而不用担心一堆页面各自的跳转逻辑变成一锅粥。路由是单页应用的骨架骨架稳了上面的功能才能搭得踏实。