前言
还在用 a 标签跳转页面?还在为 SPA 应用的路由管理头秃?本文带你深入浅出 React Router v6,从基础的路由模式到高阶的懒加载、权限守卫、嵌套路由及动态高亮,手把手教你搭建一个企业级的路由架构。文末附带炫酷的 Loading 动画实现哦!
关键词:React, React Router v6, SPA, 路由鉴权, 懒加载
以前我们是这样“切图”的:
想当年(其实也没几年),前端由于缺乏路由概念,基本就是纯纯的“切图仔”。页面跳转?那是后端大佬的事。window.location.href 或者 < a href="xxx"> 一把梭,每次跳转页面都要白屏一下,去服务器重新拉取整个 HTML。
后来,SPA(单页应用) 横空出世。前端终于站起来了!我们要掌控 URL,我们要实现“页面变了,但浏览器没刷新”的丝滑体验。
这就轮到今天的主角登场了 —— React Router。它利用 HTML5 的 History API,让前端不仅能切图,还能管理“去哪儿”的问题。
一、路由模式:Hash 还是 Browser?
在 main.jsx 中,我们通常会看到这样的包裹:
import { BrowserRouter as Router } from "react-router-dom";
createRoot(document.getElementById("root")).render(
<StrictMode>
<Router>
<App />
</Router>
</StrictMode>
);
这里有两个流派,选谁?
-
BrowserRouter (推荐) :
- 颜值:yoursite.com/about。干净又卫生。
- 原理:基于 HTML5 history.pushState API。
- 坑点:需要后端配合。不然刷新一下页面,Nginx 找不到这个路径,直接报 404。
-
HashRouter:
- 颜值:yoursite.com/#/about。那个 # 号就像脸上的青春痘,有点突兀。
- 原理:利用 URL 的锚点(Hash)变化。
- 优点:兼容性极好(连 IE 老古董都支持),不需要后端配置,纯前端自嗨。
结论:
-
做正经项目(尤其是有 SEO 需求的),请无脑选 BrowserRouter
-
如果是内部后台、个人练习项目、演示 Demo、Electron 应用,想省事不折腾,直接无脑上 HashRouter。
二、 路由配置:像搭积木一样简单
React Router v6 引入了 < Routes> 和 < Route>,比 v5 的 < Switch> 直观多了。
2.1 基础路由与 404
我们在src/router/index.jsx 里统一管理路由配置:
import {Roures,Route} from 'react-router-demo'
<Routes>
{/* 首页 */}
<Route path="/" element={<Home />} />
{/* 关于页 */}
<Route path="/about" element={<About />} />
{/* 404 页面:路径写 * 代表匹配所有剩余路由 */}
<Route path="*" element={<NotFound />} />
</Routes>
当用户迷路时,NotFound 组件不仅要提示,最好还能自动送他回家:
// pages/NotFound.jsx
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
export default function NotFound() {
const navigate = useNavigate(); // 编程式导航 hook
useEffect(() => {
// 6秒后自动回首页,这是对路痴最后的温柔
setTimeout(() => {
// 跳转到path="/"
navigate("/");
// navigate("/", { replace: true }) // 如果不想留黑历史,加上 replace
}, 6000);
}, []);
return <>404! 未找到没找到相关的路由,6秒后送你回家...</>;
}
2.2 动态路由:URL 里的秘密
电商网站经常有 /product/123 这种路径,123 是动态的。
{/* 定义参数名为 id */}
<Route path="/user/:id" element={<UserProfile />} />
组件里怎么拿?用 useParams:
// pages/UserProfile.jsx
import { useParams } from "react-router-dom";
export default function UserProfile() {
const { id } = useParams(); // 解构出来的就是 URL 里的参数
return <>正在查看 ID 为 {id} 的用户画像</>;
}
2.3 嵌套路由与 Outlet:画中画
很多后台管理系统,侧边栏和头部不变,只有中间区域在变。这时候就需要嵌套路由。
<Route path="/products" element={<Product />}>
{/* 默认子路由 */}
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
注意!父组件 Product 必须留个“坑位”给子路由渲染,这个坑位就是 < Outlet>:
// pages/Product.jsx
import { Outlet } from "react-router-dom";
export default function Product() {
return (
<div className="product-layout">
<h1>🛍️产品列表总览</h1>
<hr />
{/* 子路由渲染在这里 👇 */}
<Outlet />
</div>
);
}
2.4 重定向:新老交替
项目重构了,老路径 /old-path 不想用了,但用户收藏夹里还是旧的怎么办?用 < Navigate>:
import {Navigate} from 'react-router-dom'
<Route path="/old-path" element={<Navigate replace to="/new-path" />} />
<Route path="/new-path" element={<NewPath />} />
replace 属性很重要,它意味着“替换”当前历史记录,用户点“后退”不会死循环。
三、 进阶技巧:从入门到精通
3.1 性能优化:路由懒加载 (Lazy Loading)
如果首屏就把 Pay、Admin 这种此时根本不用的组件代码加载下来,不仅浪费流量,页面加载还慢。
React 的 lazy 和 Suspense 是绝配。
改造前:
import About from '../pages/About' (同步引入,打包进主包)
改造后:
/src/router/index.jsx
import { lazy, Suspense } from "react";
import LoadingFallback from "../components/LoadingFallback"; // 自定义的 Loading 动画
// 只有当路由匹配时,才会去网络请求这个 JS 文件
const Home = lazy(() => import("../pages/Home"));
const About = lazy(() => import("../pages/About"));
const Product = lazy(() => import("../pages/product"));
export default function RouterConfig() {
return (
// Suspense 是必须的!因为加载代码需要时间,这期间显示 fallback
<Suspense fallback={<LoadingFallback />}>
<Routes>
{/* ...路由列表 */}
</Routes>
</Suspense>
);
}
3.2 路由鉴权:闲人免进
支付页面 /pay 是 VIP 禁地,没登录不能进。我们需要写一个高阶组件 (HOC) 或者包裹组件来实现路由守卫。
守卫组件 (ProtectRoute.jsx) :
/src/components/ProtectRoute.jsx
import { Navigate } from "react-router-dom";
export default function ProtectRoute({ children }) {
// 真实项目中这里通常是读取 Redux/Zustand 中的 token
const isLoggedIn = localStorage.getItem("isLogin") === "true";
if (!isLoggedIn) {
// 没登录?去登录页呆着吧,别忘了 replace
return <Navigate to="/login" replace />;
}
// 登录了?请进
return <>{children}</>;
}
路由配置:
<Route
path="/pay"
element={
<ProtectRoute>
<Pay />
</ProtectRoute>
}
/>
3.3 手写导航高亮:精准控制
虽然 NavLink 组件自带高亮功能,但有时候我们需要更强的控制力。比如利用 useResolvedPath 和 useMatch 手写一个 active 类名判断逻辑。
看看 Navigation.jsx 里的骚操作:
import { Link, useResolvedPath, useMatch } from "react-router-dom";
// 自定义样式辅助函数
const isActive = (to) => {
const resolvedPath = useResolvedPath(to); // 解析绝对路径
// useMatch: 如果当前 URL 与 path 匹配,返回 match 对象,否则 null
const match = useMatch({
path: resolvedPath.pathname,
end: true // 精准匹配,不包含子路径
});
return match ? "active" : "";
};
// 使用
<Link to="/products" className={isActive("/products")}>Product</Link>
四、 视觉体验:拒绝枯燥的 Loading
为了让懒加载的等待时间不那么尴尬,我们手写了一个纯 CSS 的炫酷 Loading(位于 components/LoadingFallback)。
/src/components/LoadingFallback/index.jsx
import styles from "./index.module.css";
export default function LoadingFallback() {
return (
<div className={styles.container}>
<div className={styles.spinner}>
<div className={styles.circle}></div>
<div className={`${styles.circle} ${styles.circleInner}`}></div>
</div>
<p className={styles.text}>Loading...</p>
</div>
);
}
CSS样式
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-color: rgba(255, 255, 255, 0.9);
}
.spinner {
position: relative;
width: 60px;
height: 60px;
}
.circle {
position: absolute;
width: 100%;
height: 100%;
border: 4px solid transparent;
border-top-color: #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.circleInner {
width: 70%;
height: 70%;
top: 15%;
left: 15%;
border-top-color: #e74c3c;
animation: spin 0.8s linear infinite reverse;
}
/* 关键帧动画 */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.text {
margin-top: 20px;
color: #2c3e50;
font-size: 18px;
font-weight: 500;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0% {
opacity: 0.6;
}
50% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}
核心 CSS 知识点:
- @keyframes spin: 控制旋转。
- animation: 组合动画。
- border-radius: 50% : 画圆。
- border-color: 利用透明边框制造缺口效果。
/* index.module.css 部分节选 */
.circle {
border: 4px solid transparent; /* 透明底 */
border-top-color: #3498db; /* 只有顶部有色 */
border-radius: 50%; /* 变圆 */
animation: spin 1s linear infinite; /* 转起来 */
}
五、进阶小技巧:Navigate
在 React Router v6 中, 是一个非常实用且高频使用的组件。你可以把它简单理解为 “路由界的自动传送门” 。
它的核心逻辑非常简单粗暴:只要这个组件被渲染(Render)出来,页面就会立刻跳转到指定位置。
以下是它的三个核心要点:
1. 核心作用:声明式导航
与 useNavigate(用于点击事件或异步操作后的跳转)不同, 专门用于渲染逻辑中。
场景一:重定向 (Redirect)
当用户访问一个旧的路径时,自动把他传送到新路径。
你的代码 router/index.jsx 中就有这个用法:
// 当访问 /old-path 时,立刻跳转到 /new-path
<Route path="/old-path" element={<Navigate replace to="/new-path" />} />
场景二:路由守卫 (Auth Guard)
当用户没有权限查看某个页面时,直接把他踢回登录页。
components/ProtectRoute.jsx 中正是这样用的:
if (!isLoggedIn) {
// 没登录?渲染 Navigate 组件,立刻触发跳转
return <Navigate to="/login" />;
}
2. 关键属性:replace
这是 最重要的属性,没有之一。
-
不加 replace (默认) :相当于 history.push。浏览器会记录一条历史。
- 后果:用户跳转后,点浏览器的“后退”按钮,会退回到跳转前的页面,然后因为逻辑又立刻触发跳转,导致用户陷入“后退死循环” ,出不来。
-
加上 replace:相当于 history.replace。替换当前历史记录。
- 后果:用户点“后退”按钮,会直接回到上上个页面,体验更佳。
口诀:做重定向或权限拦截时,务必加上 replace。
3. 如何传递参数?
你还可以通过 state 属性偷偷带点“私货”给目标页面:
<Navigate to="/login" state={{ from: "/pay", msg: "请先买票" }} replace />
在目标页面(Login)可以通过 useLocation().state 拿到这些数据,从而实现“登录后自动跳回刚才想去的页面”。
结论
- useNavigate() :适合在 事件回调 中用(比如点按钮后跳转、请求成功后跳转)。
- :适合在 组件渲染逻辑 中用(比如判定没权限直接跳、路径变更直接跳)。
六、 总结
React Router v6 相比 v5 做了大量减法,去掉了 withRouter,用 Hooks (useNavigate, useParams, useMatch) 取代了繁琐的高阶组件,代码量更少,逻辑更清晰。
记住这几点:
- BrowserRoutrer 是首选。
- Lazy + Suspense 是性能优化的标配。
- Outlet 是嵌套路由的灵魂。
- Navigate 是重定向和守卫的好帮手。
掌握了这些,你就不再是简单的“切图仔”,而是能掌控应用流向的“前端老司机”了!