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

相关推荐
labixiong1 小时前
async/await 到底是不是 Generator 的语法糖?手写执行器,Babel 编译产物里藏着答案
前端·javascript·babel
windliang1 小时前
Claude Code 源码分析(六):上下文的发现、注入与压缩
前端·javascript·人工智能
张龙6871 小时前
10 万条数据不卡顿:不定高虚拟列表从原理到生产实现
前端·javascript·性能优化
玉鸯1 小时前
界面用完即消失:Agent 生成式 UI 的短暂性哲学与前端工程的未来
前端·llm·agent
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十四):管理员个人资料页面、管理员日志优化
前端·后端·go
陆枫Larry2 小时前
并发、并行、竞态的区别梳理
前端
水煮白菜王2 小时前
商用地图全面收费?从天地图到开源生态的替代路线
前端·javascript·高德地图·amap·开源地图
程序员黑豆2 小时前
鸿蒙应用开发:网络请求三种方式详解(http / rcp / axios)
前端·harmonyos