摘要
拆解BrowserRouter、Outlet嵌套路由、useParams、ProtectRoute鉴权、useNavigate导航、lazy懒加载,覆盖React Router v7全场景。
前两篇文章分别从零实现了 HashRouter 和 HistoryRouter,理解了 hashchange、pushState、popstate 这些底层机制。但在实际项目中,不会每次都手写路由------React Router 将这些底层能力封装成了声明式、组件化的 API,是 React 生态中前端路由的事实标准。
本文基于一个 React 19 + react-router-dom v7 的完整项目,逐层拆解 React Router 的核心能力:从基础的路由配置,到嵌套路由、动态参数、鉴权守卫、懒加载,覆盖生产级 SPA 中的全场景需求。
项目骨架与依赖
json
{
"dependencies": {
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-router-dom": "^7.18.2"
},
"devDependencies": {
"vite": "^8.0.12",
"@vitejs/plugin-react": "^6.0.1"
}
}
项目使用 Vite 8 作为构建工具,入口是标准的 index.html → src/main.jsx。页面组件统一放在 pages/ 目录下,每个页面一个文件,路由守卫和导航组件独立管理。
BrowserRouter:选择 History 模式
React Router 提供了两种顶层路由容器:
jsx
import { BrowserRouter as Router } from 'react-router-dom';
BrowserRouter 底层使用 History API(pushState + popstate),URL 干净无 # 号。与之对应的 HashRouter 则使用 hashchange。生产项目中,面向用户的 C 端产品几乎都选择 BrowserRouter------URL 美观、SEO 友好,代价是需要服务端配置 fallback(所有路由返回 index.html)。
BrowserRouter 包裹整个应用,内部的 Routes 和 Route 组件才能正常工作。
Routes 与 Route:声明式路由配置
React Router 的核心设计理念是路由即组件。路由配置不是 JSON 也不是独立文件,而是直接写在 JSX 中:
jsx
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<UserProfile />} />
<Route path="/products" element={<Products />}>
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
<Route path="/login" element={<Login />} />
<Route path="/pay" element={
<ProtectRoute>
<Pay />
</ProtectRoute>
} />
<Route path="*" element={<NotFound />} />
</Routes>
Routes 是路由匹配引擎,它遍历所有子 Route,找到第一个匹配当前 URL 的路径,渲染对应的 element。同一时刻有且只有一个 Route 被激活。
Route 的 path 属性指定匹配规则,element 属性指定渲染内容。element 可以是任意 React 组件,这就为路由守卫、布局组件等模式提供了灵活的基础。
动态路由参数:useParams
URL 中经常需要携带动态参数,比如用户 ID、商品 ID。React Router 使用 :paramName 语法定义动态段:
jsx
<Route path="/user/:id" element={<UserProfile />} />
在组件内部,通过 useParams Hook 获取参数值:
jsx
import { useParams } from 'react-router-dom';
function UserProfile() {
let { id } = useParams();
return <h2>User Profile:{id}</h2>;
}
当用户访问 /user/123 时,id 的值为 "123"。useParams 返回的是一个对象,键名对应路径中 :paramName 的名称。注意返回值始终是字符串类型,如需数字需要自行转换。
嵌套路由与 Outlet
嵌套路由是 React Router 最强大的特性之一。当页面存在"布局 + 子内容"的结构时------比如产品列表页顶部有统一的标题和筛选栏,下方根据 URL 切换详情页或新增页------嵌套路由是天然的选择。
jsx
// 父路由组件
import { Outlet } from 'react-router-dom';
const Products = () => {
return (
<>
<h1>产品列表</h1>
<Outlet />
</>
);
};
Outlet 是嵌套路由的"插槽"------子路由匹配成功后,对应的组件会渲染在 Outlet 所在的位置。
jsx
// 路由配置:子路由 path 相对于父路由
<Route path="/products" element={<Products />}>
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
访问 /products/123 时,Products 组件渲染,Outlet 处显示 ProductDetail;访问 /products/new 时,Outlet 处显示 NewProduct。父路由的 UI(标题、布局)保持不变,只有子区域切换。
子路由的 path 是相对于父路由 的,不需要写 /products/:productId,直接写 :productId 即可。
Link 组件:SPA 中的导航
在 SPA 中,不能直接使用 <a> 标签做页面跳转------<a> 会触发浏览器的完整页面请求,导致 SPA 状态丢失。React Router 提供了 Link 组件替代:
jsx
import { Link } from 'react-router-dom';
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/user/123">小家</Link></li>
<li><Link to="/products/123">商品详情</Link></li>
</ul>
</nav>
Link 组件渲染为 <a> 标签,但 to 属性被拦截处理------点击时调用 history.pushState 改变 URL,由 React Router 接管渲染,页面不会刷新。
编程式导航:useNavigate 与 useLocation
有些场景不适合用 Link------比如表单提交后跳转、超时后自动跳转、登录成功后跳转。这时需要编程式导航。
jsx
import { useNavigate, useLocation } from 'react-router-dom';
const Login = () => {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from || '/';
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 });
}
}
return (
<form onSubmit={handleSubmit}>
<input name="username" placeholder="请输入用户名" required />
<input name="password" placeholder="请输入密码" required />
<button type="submit">登录</button>
</form>
);
};
useNavigate 返回一个跳转函数,调用 navigate('/path') 等价于 history.pushState + 渲染。第二个参数 { replace: true } 使用 replaceState 替换当前历史记录------登录成功后,用户点击后退按钮不会回到登录页,而是直接回到登录前的页面,体验更自然。
useLocation 返回当前 URL 的 location 对象,其中 location.state 携带了路由跳转时传递的额外数据。配合路由守卫,可以实现"从未登录页跳转到登录页,登录后自动回到原页面"的完整闭环。
useNavigate 也适用于定时跳转场景:
jsx
const NotFound = () => {
let navigate = useNavigate();
useEffect(() => {
setTimeout(() => navigate('/'), 3000);
}, []);
return <>Not Found</>;
};
路由守卫:ProtectRoute 组件
某些页面需要登录后才能访问(如支付页、个人中心)。React Router 没有内置的"路由守卫"概念,但利用其组件化特性,可以轻松实现:
jsx
import { Navigate } from 'react-router-dom';
const ProtectRoute = ({ children }) => {
const isLogin = localStorage.getItem('isLogin') === 'true';
if (!isLogin) {
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
return <>{children}</>;
};
ProtectRoute 是一个包装组件 :它接收 children 作为需要保护的页面内容。检查登录状态后,未登录时渲染 <Navigate> 组件进行重定向,已登录时渲染 children。
使用方式:
jsx
<Route path="/pay" element={
<ProtectRoute>
<Pay />
</ProtectRoute>
} />
Navigate 组件渲染时,React Router 会立即将 URL 重定向到 /login,并携带 state 对象,其中 from 记录了用户原本想访问的路径。登录组件通过 useLocation().state?.from 取回这个路径,登录成功后 navigate(from, { replace: true }) 跳转回去。
这个守卫模式的核心思路是:路由守卫不拦截路由匹配,而是在组件渲染层面做条件判断 。/pay 的路由始终匹配,但渲染的内容由 ProtectRoute 根据鉴权状态决定------要么是 Pay 组件,要么是重定向。
路由懒加载:React.lazy + Suspense
随着项目增长,把所有页面组件打包进一个 JS 文件会导致首屏加载缓慢。React Router 配合 React 的 lazy 和 Suspense 可以实现按路由拆分代码:
jsx
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/Products/Detail'));
const NewProduct = lazy(() => import('./pages/Products/New'));
const Login = lazy(() => import('./pages/Login'));
const Pay = lazy(() => import('./pages/Pay'));
const NotFound = lazy(() => import('./pages/NotFound'));
const ProtectRoute = lazy(() => import('./ProtectRoute'));
lazy(() => import(...)) 是动态导入,Vite 会将其识别为代码分割点,为每个页面生成独立的 chunk。用户访问 /about 时,浏览器只加载 About 对应的 chunk,而不是整个应用。
Suspense 提供加载中的 fallback UI:
jsx
<Router>
<Suspense fallback={<div>loading...</div>}>
<Navigation />
<Routes>
{/* ... */}
</Routes>
</Suspense>
</Router>
在懒加载组件还未下载完成时,Suspense 的 fallback 会显示。下载完成后自动替换为真实组件。
重定向与 404 兜底
项目中有两种重定向场景:
静态重定向------旧路径永久迁移到新路径:
jsx
<Route path="old-path" element={<Navigate replace to="new-path" />} />
用户访问 /old-path 时,Navigate 组件渲染,URL 立即变为 /new-path,replace 确保旧路径不会留在历史记录中。
404 兜底------匹配所有未定义的路径:
jsx
<Route path="*" element={<NotFound />} />
* 是通配符,匹配所有未被前面 Route 匹配的路径。它必须放在 Routes 的最后 ,因为 Routes 按顺序匹配,找到第一个匹配的 Route 就停止。如果 * 放在最前面,所有路径都会被它吞掉。
总结:React Router 全场景能力地图
| 能力 | 核心 API | 使用场景 |
|---|---|---|
| 路由容器 | BrowserRouter / HashRouter |
顶层包裹,选择 History 或 Hash 模式 |
| 路由配置 | Routes + Route |
声明式配置路径与组件的映射 |
| 导航组件 | Link |
替代 <a> 标签,实现无刷新跳转 |
| 动态参数 | useParams |
从 URL 中提取 :id 等动态段 |
| 嵌套路由 | Outlet |
父路由提供布局,子路由渲染到插槽 |
| 编程式导航 | useNavigate |
表单提交后跳转、定时跳转 |
| 路由信息 | useLocation |
获取当前路径、state 传递的额外数据 |
| 路由守卫 | 自定义包装组件 + Navigate |
鉴权拦截,未登录重定向到登录页 |
| 重定向 | Navigate 组件 |
旧路径迁移、登录后跳回 |
| 懒加载 | React.lazy + Suspense |
按路由拆分代码,减少首屏加载 |
| 404 处理 | <Route path="*"> |
匹配所有未定义路径,兜底展示 |
从手写 HashRouter 理解 hashchange 事件,到手写 HistoryRouter 理解 pushState 和 popstate,再到 React Router 将这些底层机制封装为声明式 API------前两篇文章建立的底层认知,是理解 React Router 每行配置背后"发生了什么"的关键。当你看到 Link 组件,你知道它底层调用了 history.pushState;当你看到 Navigate 组件,你知道它本质上是条件渲染 + replaceState;当你看到 ProtectRoute,你知道它不过是利用组件化特性实现的一个高阶守卫模式。
React Router v7 的 API 设计将"路由即组件"的理念贯彻到底------路由配置是 JSX,路由守卫是组件,重定向是组件,连懒加载都是 React 原生能力。这种一致性让 React Router 的学习曲线平滑,但只有理解底层原理,才能在遇到问题时知道从哪个方向排查。毕竟,所有前端路由框架的底层,最终都收敛到 hashchange、pushState、replaceState 和 popstate 这四个 API 上。