React Router 从入门到实战:一篇搞定路由配置、懒加载与嵌套路由

路由是单页应用(SPA)的骨架,读懂了路由,你就掌握了页面切换的主动权。

为什么前端路由是"被逼出来"的?

老规矩,咱们先聊点历史。以前做 Web 开发,页面跳转全靠后端路由------你点个链接,浏览器向服务器发请求,服务器返回新页面,白屏、闪烁、等待......用户体验妥妥的"复古风"。

后来有了 Ajax,有了前后端分离,前端开始自己管页面切换。SPA(单页应用) 登上舞台,核心需求就是:切换页面但不能刷新浏览器

怎么实现?最早期用的是 hash(锚链接)。改变 URL 的 # 后面的部分不会触发页面刷新,还能被 hashchange 事件监听到。于是 #/home#/about 成了前端路由的雏形。

现在 React 生态中,react-router-dom 是事实上的路由标准。它帮我们封装了历史管理、路径匹配、组件渲染等复杂逻辑,我们只需要配置好路由表,剩下的交给它。

路由三兄弟:HashRouter、BrowserRouter、Routes

react-router-dom 里,最常用的两种路由器:

  • HashRouter :依赖 URL 的 # 部分,兼容性好,适合静态站点或不想折腾服务器配置的场景。
  • BrowserRouter :依赖 HTML5 History API,路径干净(没有 #),但需要服务器配合(所有路由都返回 index.html)。

我们的示例用的是 HashRouter,因为简单、零配置。

核心组件就三个:

  • <Routes>:容器,负责匹配当前路径,渲染对应的 <Route>
  • <Route>:配置项,path 定义路径,element 定义要渲染的组件。
  • <Navigate>:重定向,替换 Redirect(老版本)。

来看一份基础配置:

javascript 复制代码
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Router>
  );
}

没错,就这么简单。只要路径匹配,对应的组件就会渲染在 <Routes> 的位置。

懒加载(lazy)+ Suspense:让首屏飞起来

"下载所有页面代码再渲染"是 SPA 的常见性能坑。解决办法就是路由懒加载------当前路由的组件才加载,其他路由的代码先放一边。

React 官方提供了 lazy + Suspense 组合拳。

javascript 复制代码
import { lazy, Suspense } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

懒加载原理浅析

lazy 函数接收一个动态 import() 调用,它返回一个 Promise,这个 Promise 会解析为一个 React 组件模块。lazy 内部会利用 React 的 Suspense 机制,在组件渲染时"挂起"渲染,直到 Promise 完成。

关键点

  • lazy 组件必须被 <Suspense> 包裹,否则会抛出错误。
  • <Suspense> 可以放在路由器的内部或外部,但必须覆盖到所有懒加载组件。
  • fallback 属性接收任何 React 元素,在组件加载期间显示(通常是 Loading 动画)。

最佳实践 :建议将 <Suspense> 放在路由配置的最外层,这样所有路由懒加载组件都能被捕获,并且统一显示 Loading 效果。如果你有多个路由分组,也可以在每个分组外面单独包 <Suspense>,但通常全局一个就够了。

懒加载是性能优化的第一站,它让用户"所见即所得",而不是"所得即所有"。

动态路由与 useParams:URL 里的参数这样拿

用户详情页通常路径是 /user/123,其中的 123 是动态的。定义动态路由用 :参数名

ini 复制代码
<Route path="/user/:id" element={<UserProfile />} />

在组件里通过 useParams 钩子获取:

javascript 复制代码
import { useParams } from 'react-router-dom';

function UserProfile() {
  const { id } = useParams();
  return <h2>User Profile: {id}</h2>;
}

useParams 深入理解

useParams 返回一个对象,键是路由路径中定义的那些动态参数名,值是当前 URL 中对应的实际值。比如访问 /user/123useParams() 返回 { id: '123' }

重要特性

  • 当 URL 改变且参数变化时,组件会重新渲染,useParams 返回新的值。
  • 如果当前路由没有动态参数,返回空对象 {}
  • 它是基于 React Context 实现的,所以必须在 <Router> 内部使用。

注意 :如果父路由有动态参数,子路由里调用 useParams 只会获取到当前路由 的参数,如果想获取父路由的参数,可以通过 useParams 在父组件中获取并通过 props 传递,或者使用 useOutletContext

嵌套路由与 Outlet:布局复用利器

很多后台管理系统的布局是顶部导航 + 侧边栏 + 内容区,而内容区会根据路由切换。这时候嵌套路由就派上用场了。

父路由 定义公共布局,并在内容区放置 <Outlet /> 作为子路由的出口。

javascript 复制代码
// Products 组件(父路由)
import { Outlet } from 'react-router-dom';

function Products() {
  return (
    <>
      <h1>产品列表</h1>
      <Outlet />  {/* 子路由会渲染在这里 */}
    </>
  );
}

路由配置

xml 复制代码
<Route path="/products" element={<Products />}>
  <Route path=":productId" element={<ProductDetail />} />
  <Route path="new" element={<NewProduct />} />
</Route>

访问 /products/123 时,Products 渲染,其内部 <Outlet /> 会渲染 ProductDetail 组件。访问 /products/new 则渲染 NewProduct

子路由的路径是相对于父路由的(不需要再写 /products/)。

在子组件中,依然可以通过 useParams 获取动态参数:

javascript 复制代码
import { useParams } from 'react-router-dom';

function ProductDetail() {
  const { productId } = useParams();
  return <h3>产品详情 {productId}</h3>;
}

这种结构让页面复用得心应手,强烈推荐在后台项目中使用。

编程式导航:useNavigate vs window.location.href

有时候我们需要在事件处理、异步请求完成后跳转页面,这时候就要用到编程式导航 。React Router 提供了 useNavigate 钩子。

javascript 复制代码
import { useNavigate } from 'react-router-dom';

function SomeComponent() {
  const navigate = useNavigate();

  const handleClick = () => {
    navigate('/about'); // 跳转到 /about
  };

  const handleSubmit = async () => {
    await saveData();
    navigate('/success', { replace: true }); // 替换当前历史记录
  };

  return <button onClick={handleClick}>跳转</button>;
}

useNavigate 用法详解

  • navigate(to, options)to 是目标路径字符串或 -1(后退),options 可包含 replacestate 等。
  • replace: true 表示替换当前历史条目,而不是新增一条,适用于登录跳转等场景,防止用户回退到登录页。
  • state 可以在目标路由中用 useLocation 获取,用于传递隐式数据。

为什么不用 window.location.href?

ini 复制代码
window.location.href = '/about';  // ❌ 不推荐
navigate('/about');               // ✅ 推荐
  • window.location.href 会触发浏览器整页刷新,重新加载 HTML、JS、CSS,完全破坏了 SPA 的体验。
  • navigate 基于路由内部的历史管理,仅更新 UI 部分,无刷新切换,更快、更流畅。
  • 在 React Router 环境中,务必使用 useNavigate<Link>,不要用原生跳转方式。

在 404 页面中实现倒计时跳转

这里用 useNavigate 实现 3 秒后返回首页:

javascript 复制代码
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

function NotFound() {
  const navigate = useNavigate();
  useEffect(() => {
    const timer = setTimeout(() => {
      navigate('/'); // 3 秒后跳转首页
    }, 3000);
    return () => clearTimeout(timer);
  }, [navigate]);
  return <div>页面不存在,3秒后返回首页</div>;
}

404 与重定向:优雅地处理"迷路"和"搬家"

用户输入了不存在的路径,总不能给个空白页吧?用 * 通配符匹配所有未定义路径,指向 404 组件。

ini 复制代码
<Route path="*" element={<NotFound />} />

但更常见的是,我们需要对某些旧路径进行重定向,把用户从旧地址自动带到新地址。

react-router-dom 提供了 <Navigate> 组件,用于在渲染时立即执行跳转。它的用法非常简洁:

ini 复制代码
<Route path="/old-path" element={<Navigate replace to="/new-path" />} />

当用户访问 /old-path 时,<Navigate> 会被渲染,并立即触发跳转到 /new-path

关键属性解读

  • to:目标路径(字符串或对象,如 { pathname: '/about', search: '?tab=1' })。
  • replace:布尔值。如果为 true,替换当前历史记录(相当于 history.replace),这样用户点击"后退"不会回到 /old-path;如果为 false(默认),则新增一条历史记录。

为什么 replace 很重要?

设想一个场景:旧首页 /home 被重定向到新首页 /dashboard。如果用默认的 replace=false,用户访问 /home 后,浏览器历史记录中有两条:/home/dashboard。点击后退会回到 /home,然后又被重定向到 /dashboard,形成死循环。设置 replace=true 则直接替换,历史记录只有 /dashboard,后退就回到上一个真实页面,体验更合理。

重定向的常见应用场景

  1. 旧 URL 迁移:网站改版后,保留旧路径的访问能力,自动跳转到新路径。
  2. 默认路由 :比如访问根路径 / 时,重定向到 /home/dashboard
  3. 权限控制:未登录用户访问需鉴权的页面时,重定向到登录页。
  4. 404 降级:某些情况下,可以用重定向代替 404,例如将废弃的产品详情页跳转到产品列表页。

来看一个组合示例:

xml 复制代码
<Routes>
  {/* 根路径跳转到首页 */}
  <Route path="/" element={<Navigate replace to="/home" />} />
  
  {/* 旧地址迁移 */}
  <Route path="/old-about" element={<Navigate replace to="/about" />} />
  
  {/* 权限守卫(示意) */}
  <Route path="/admin" element={
    isLogin ? <AdminPanel /> : <Navigate replace to="/login" />
  } />
  
  {/* 404 兜底 */}
  <Route path="*" element={<NotFound />} />
</Routes>

注意<Navigate> 是一个"立即执行"的组件,所以它必须放在 <Routes> 内部,并且匹配到路径才会触发。如果放在任何地方都会跳转,那就成了"全局重定向"了,要小心使用。

对比编程式重定向(useNavigate)与声明式重定向(Navigate)

  • 声明式(<Navigate>)适合"路径到路径"的固定映射,简洁直观。
  • 编程式(navigate)适合"条件触发"的场景,比如点击按钮后、异步请求完成后跳转。

二者底层都是调用相同的路由跳转 API,根据场景选择即可。

在 SPA 中,禁止使用 <a href="/about"> ,因为它会触发浏览器整页刷新,破坏 SPA 特性。必须使用 react-router-dom 提供的 <Link> 组件。

javascript 复制代码
import { Link } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/user/123">用户</Link>
    </nav>
  );
}

<Link> 会渲染成 <a>,但拦截了点击事件,使用路由的历史管理进行无刷新跳转。同时它还支持 state 传递隐式数据,非常实用。

结语 & 最佳实践

  • 路由配置集中化 :建议将路由配置抽离成单独的文件(如 routes.js),便于维护。
  • 懒加载按需分割 :每个页面组件都使用 lazy 包裹,配合 Webpack 的代码分割,首屏加载速度显著提升。
  • 嵌套路由设计:根据页面布局规划嵌套层级,避免过深(一般两层足够)。
  • 404 放最后* 路由必须放在所有具体路由之后,否则它会拦截所有请求。
  • 用好 hooksuseParamsuseNavigateuseLocation 等钩子让路由操作变得无比轻松。
  • Suspense 最佳实践 :通常在外层只放一个 <Suspense>,并提供一个全局的 Loading 组件。如果有特殊需求(比如某些区域单独加载),可以再拆分。
  • 重定向时注意 replace :避免无限循环,养成使用 replace 的好习惯。

React Router 就像应用的导航系统,设计得好,开发体验流畅,用户操作丝滑。希望这份实战指南能帮你轻松驾驭它。

相关推荐
刺客码9 小时前
Layui 表格固定列行高错位问题解决方案
前端框架·layui·jquery·web
Captaincc9 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师11 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen11 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒11 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow12 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81812 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007812 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明12 小时前
第 6 章 组件进阶
前端·vue.js
江华森12 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端