React Router 全家桶实战:从路由懒加载到嵌套路由的 6 大核心玩法
SPA 单页应用的核心是前端路由,而 React 生态中最成熟的路由方案就是
react-router-dom。本文从 RESTful 资源理念出发,带你完整掌握 React Router 的 基本路由配置、路由懒加载、动态路由参数、嵌套路由、路由重定向、404 兜底 六大核心玩法。全文代码可直接运行,建议收藏后动手实践。
一、从 RESTful 理念理解前端路由
1.1 一切皆资源
RESTful 的核心思想是一切皆资源,每个 URL 对应一个唯一的资源:
bash
GET /users → 用户列表资源
GET /users/123 → 用户 123 的详情资源
POST /users → 创建用户资源
GET /products → 商品列表资源
GET /products/45 → 商品 45 的详情资源
前端路由沿用了这个理念------每个 URL 对应一个页面(资源):
bash
/ → 首页
/about → 关于页面
/user/:id → 用户详情页
/products → 商品列表页
/products/:id → 商品详情页
/products/new → 新建商品页
1.2 前端路由 vs 后端路由
bash
后端路由(传统多页应用):
浏览器请求 /about
│
▼
服务器返回 about.html
│
▼
浏览器整页重新渲染(白屏)
前端路由(SPA 单页应用):
浏览器请求 / (只加载一次 index.html)
│
▼
用户点击 /about 链接
│
▼
hash 变化 → hashchange 事件
│
▼
JS 监听到变化,替换 #container 内容(无白屏)
| 维度 | 后端路由 | 前端路由 |
|---|---|---|
| 页面加载 | 整页重新加载 | 局部 DOM 替换 |
| 用户体验 | 白屏闪烁 | 丝滑流畅 |
| 服务器负担 | 每次返回完整 HTML | 首次加载后不再请求 HTML |
| 适用场景 | 传统网站 | 现代 SPA 应用 |
1.3 React 全家桶中的路由定位
React 开发"全家桶"由三部分组成:
┌─────────────────────────────────────────────┐
│ React 全家桶 │
│ │
│ ┌─────────────┐ UI 界层 │
│ │ React │ 组件开发、响应式、Hooks │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ 路由层 │
│ │ react-router │ SPA 页面切换、路由管理 │
│ │ -dom │ │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ 状态层 │
│ │ Zustand / │ 全局状态管理 │
│ │ Pinia │ │
│ └─────────────┘ │
└─────────────────────────────────────────────┘
二、环境准备与项目结构
2.1 安装依赖
bash
npm install react-router-dom
2.2 项目结构
bash
src/
├── App.jsx # 路由配置入口
├── components/
│ └── Navigation.jsx # 导航栏组件
├── pages/
│ ├── Home.jsx # 首页
│ ├── About.jsx # 关于页面
│ ├── UserProfile.jsx # 用户详情页(动态路由)
│ ├── Products/
│ │ ├── index.jsx # 商品列表页(嵌套路由父级)
│ │ ├── Detail.jsx # 商品详情页(二级路由)
│ │ └── New.jsx # 新建商品页(二级路由)
│ └── NotFound.jsx # 404 页面
三、六大核心玩法完整实现
3.1 完整路由配置代码
jsx
import { lazy, Suspense } from 'react';
import {
HashRouter as Router, // 基于 hashchange 的前端路由
Routes, // 路由配置容器
Route, // 单个路由配置项
Navigate // 路由重定向组件
} from 'react-router-dom';
import Navigation from './components/Navigation';
// ===== 路由懒加载:按需加载页面组件 =====
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const NotFound = lazy(() => import('./pages/NotFound'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/Products/Detail'));
const NewProduct = lazy(() => import('./pages/Products/New'));
const App = () => {
return (
<>
{/* Router 接管整个应用的前端路由 */}
<Router>
{/* Suspense 包裹懒加载组件,加载期间显示 fallback */}
<Suspense fallback={<div>Loading...</div>}>
<Navigation />
{/* 路由出口:当前 URL 对应的页面组件会渲染到这里 */}
<div id="container">
<Routes>
{/* ① 基本路由:精确匹配 path */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* ② 动态路由::id 是路由参数 */}
<Route path="/user/:id" element={<UserProfile />} />
{/* ③ 嵌套路由:父路由 + 子路由 */}
<Route path="/products" element={<Products />}>
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
{/* ④ 路由重定向:旧路径跳转到新路径 */}
<Route path="/old-path" element={<Navigate replace to="/about" />} />
{/* ⑤ 404 兜底:* 通配所有未匹配的路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</Suspense>
</Router>
</>
);
};
export default App;
四、核心玩法逐一深入
4.1 玩法一:基本路由配置
jsx
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
三个核心组件的职责:
| 组件 | 职责 | 类比 |
|---|---|---|
Router |
路由容器,接管整个应用 | 交通指挥中心 |
Routes |
路由配置容器,决定哪个 Route 显示 | 路线图 |
Route |
单个路由配置项,URL → 组件的映射 | 一条路线 |
工作原理:
arduino
URL 变化(hashchange)
│
▼
Router 监听到变化
│
▼
Routes 遍历所有 Route
│
▼
找到 path 匹配的 Route
│
▼
渲染对应 element 组件到 #container
关键规则:<Routes> 中有且只有一个 <Route> 会被渲染------就是当前 URL 匹配的那一个。
4.2 玩法二:路由懒加载
jsx
// ❌ 传统导入:所有页面一次性加载
import Home from './pages/Home';
import About from './pages/About';
// 首页加载时就把所有页面代码都下载了,慢!
// ✅ 懒加载导入:按需加载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
// 只有访问对应路由时才下载该页面的代码
// 必须配合 Suspense 使用
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
为什么需要懒加载?
bash
不使用懒加载:
┌──────────────────────────────────────┐
│ 首次加载 index.html │
│ 下载:Home + About + UserProfile │
│ + Products + NotFound + ... │
│ 打包体积:800KB │
│ 首屏加载时间:3.2s │
└──────────────────────────────────────┘
使用懒加载:
┌──────────────────────────────────────┐
│ 首次加载 index.html │
│ 下载:Home + Navigation │
│ 打包体积:120KB │
│ 首屏加载时间:0.8s │
│ │
│ 访问 /about 时才下载 About.js │
│ 访问 /products 时才下载 Products.js │
└──────────────────────────────────────┘
lazy + Suspense 工作流程:
xml
用户访问 /about
│
▼
React 发现 About 组件是 lazy 导入
│
▼
React 发起网络请求下载 About.js
│
▼
下载期间:显示 <Suspense fallback={<div>Loading...</div>} />
│
▼
下载完成:渲染 <About /> 组件
4.3 玩法三:动态路由参数
jsx
// 路由配置
<Route path="/user/:id" element={<UserProfile />} />
// UserProfile 组件中获取参数
import { useParams } from 'react-router-dom';
const UserProfile = () => {
const { id } = useParams(); // 从 URL 中提取 id 参数
return <h1>用户 {id} 的个人主页</h1>;
};
// URL: /user/123 → id = "123"
// URL: /user/abc → id = "abc"
动态路由匹配规则:
bash
路由模式 URL 匹配结果
/user/:id /user/123 ✅ id = "123"
/user/:id /user/abc ✅ id = "abc"
/user/:id /user/ ❌ 缺少参数
/user/:id /user/123/posts ❌ 多余路径
/user/:id/:tab /user/123/posts ✅ id="123", tab="posts"
4.4 玩法四:嵌套路由
jsx
// 父路由配置
<Route path="/products" element={<Products />}>
{/* 子路由使用相对路径 */}
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
// 父组件 Products 中必须使用 <Outlet /> 渲染子路由
import { Outlet, Link } from 'react-router-dom';
const Products = () => {
return (
<div>
<h1>商品管理</h1>
<nav>
<Link to="new">新建商品</Link>
<Link to="123">商品详情</Link>
</nav>
{/* Outlet 是子路由的渲染出口 */}
<Outlet />
</div>
);
};
嵌套路由的 URL 对应关系:
xml
URL 父组件渲染 子组件渲染
/products <Products /> ❌ 无子路由匹配
/products/123 <Products /> <ProductDetail /> (productId="123")
/products/new <Products /> <NewProduct />
嵌套路由渲染结构:
xml
<div id="container">
┌─────────────────────────────────┐
│ <Products /> (父路由组件) │
│ ┌───────────────────────────┐ │
│ │ <h1>商品管理</h1> │ │
│ │ <nav>导航链接</nav> │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ <Outlet /> │ │ │
│ │ │ 子路由组件渲染在这里 │ │ │
│ │ │ <ProductDetail /> │ │ │
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
4.5 玩法五:路由重定向
jsx
import { Navigate } from 'react-router-dom';
// 旧路径重定向到新路径
<Route path="/old-path" element={<Navigate replace to="/about" />} />
// 用户访问 /old-path 时:
// 1. 自动跳转到 /about
// 2. replace 表示替换浏览历史(用户点后退不会回到 /old-path)
replace vs push 的区别:
bash
push(默认):
浏览历史:[/] → [/old-path] → [/about]
用户点后退:从 /about 回到 /old-path → 又重定向到 /about
❌ 死循环
replace:
浏览历史:[/] → [/about]
用户点后退:从 /about 回到 /
✅ 正常
| 场景 | 选择 | 原因 |
|---|---|---|
| 旧路径迁移 | replace |
避免后退死循环 |
| 登录后跳转 | replace |
不需要回到登录页 |
| 页面间导航 | push(默认) |
保留浏览历史 |
4.6 玩法六:404 兜底
jsx
// * 通配所有未匹配的路由,必须放在最后
<Route path="*" element={<NotFound />} />
// NotFound 组件
import { Link } from 'react-router-dom';
const NotFound = () => {
return (
<div>
<h1>404 - 页面不存在</h1>
<p>你访问的页面不存在</p>
<Link to="/">返回首页</Link>
</div>
);
};
匹配优先级:
ini
Routes 会从上到下匹配,命中一个就停止
<Route path="/" /> ← 精确匹配 /
<Route path="/about" /> ← 精确匹配 /about
<Route path="/user/:id" /> ← 匹配 /user/任意值
<Route path="/products"> ← 匹配 /products 及其子路由
<Route path="/old-path" /> ← 精确匹配 /old-path
<Route path="*" /> ← 兜底:匹配以上都不命中的所有 URL
所以 * 必须放在最后!
五、导航组件的实现
jsx
// components/Navigation.jsx
import { Link, NavLink } from 'react-router-dom';
const Navigation = () => {
return (
<header>
<nav>
<ul>
{/* Link:普通导航链接 */}
<li><Link to="/">首页</Link></li>
<li><Link to="/about">关于</Link></li>
<li><Link to="/user/123">用户 123</Link></li>
<li><Link to="/products">商品管理</Link></li>
{/* NavLink:带 active 状态的导航链接 */}
<li>
<NavLink
to="/about"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
关于(高亮当前)
</NavLink>
</li>
</ul>
</nav>
</header>
);
};
export default Navigation;
Link vs NavLink:
| 组件 | 特点 | 适用场景 |
|---|---|---|
Link |
简单导航,无状态 | 普通跳转链接 |
NavLink |
自动添加 active 状态 | 导航栏高亮当前页 |
六、HashRouter vs BrowserRouter
jsx
// HashRouter:URL 带 # 号
import { HashRouter as Router } from 'react-router-dom';
// URL: https://example.com/#/about
// BrowserRouter:URL 无 # 号
import { BrowserRouter as Router } from 'react-router-dom';
// URL: https://example.com/about
| 对比 | HashRouter | BrowserRouter |
|---|---|---|
| URL 形态 | /#/about |
/about |
| 美观度 | ❌ 有 # 号 | ✅ 干净 |
| 服务器配置 | 不需要 | 需要 fallback 到 index.html |
| 原理 | hashchange 事件 | History API (pushState) |
| 兼容性 | IE8+ | IE10+ |
| 部署难度 | 简单 | 需配置 nginx |
BrowserRouter 服务器配置示例(nginx):
nginx
location / {
try_files $uri $uri/ /index.html;
}
七、六大玩法全景总结
7.1 路由配置速查表
jsx
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{/* ① 基本路由 */}
<Route path="/" element={<Home />} />
{/* ② 动态路由 */}
<Route path="/user/:id" element={<UserProfile />} />
{/* ③ 嵌套路由 */}
<Route path="/products" element={<Products />}>
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
{/* ④ 重定向 */}
<Route path="/old-path" element={<Navigate replace to="/about" />} />
{/* ⑤ 404 兜底 */}
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</Router>
7.2 核心知识点对照表
| 知识点 | API | 作用 |
|---|---|---|
| 路由容器 | HashRouter / BrowserRouter |
接管应用路由 |
| 路由配置 | Routes + Route |
URL → 组件映射 |
| 懒加载 | lazy + Suspense |
按需加载页面,优化首屏 |
| 动态参数 | :id + useParams() |
URL 中提取参数 |
| 嵌套路由 | 父 Route + 子 Route + Outlet |
多级路由结构 |
| 重定向 | Navigate |
旧路径跳转新路径 |
| 404 兜底 | path="*" |
匹配所有未定义路由 |
| 导航链接 | Link / NavLink |
声明式导航 |
7.3 数据流全景图
xml
用户点击 <Link to="/products/123">
│
▼
Router 监听到 URL 变化(hashchange / popstate)
│
▼
Routes 遍历路由表,匹配 /products/:productId
│
▼
渲染 <Products />(父路由组件)
│
▼
<Products /> 内部的 <Outlet /> 渲染 <ProductDetail />
│
▼
如果组件是 lazy 加载,先显示 <Suspense fallback>
│
▼
下载完成后渲染实际组件
八、总结
8.1 React Router 的设计哲学
markdown
1. 一切皆组件
Router、Routes、Route、Link、Navigate 都是组件
用 JSX 声明式配置路由
2. 路由即状态
URL 是应用状态的映射
路由变化 = 状态变化 = UI 更新
3. 按需加载
lazy + Suspense 实现代码分割
只加载当前需要的页面
4. 组合优于继承
嵌套路由通过组件组合实现
<Outlet /> 是子路由的渲染出口
8.2 路由类型速查
| 路由类型 | 语法 | 示例 URL | 用途 |
|---|---|---|---|
| 精确路由 | path="/about" |
/about |
固定页面 |
| 动态路由 | path="/user/:id" |
/user/123 |
带参数的页面 |
| 嵌套路由 | 父+子 Route |
/products/123 |
多级页面结构 |
| 重定向 | Navigate to=... |
/old → /new |
路径迁移 |
| 通配路由 | path="*" |
任意未匹配 URL | 404 兜底 |
如果这篇文章对你有帮助,欢迎点赞 和收藏!