路由是单页应用(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/123,useParams() 返回 { 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可包含replace、state等。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 />} />
但更常见的是,我们需要对某些旧路径进行重定向,把用户从旧地址自动带到新地址。
Navigate 组件:声明式重定向
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,后退就回到上一个真实页面,体验更合理。
重定向的常见应用场景
- 旧 URL 迁移:网站改版后,保留旧路径的访问能力,自动跳转到新路径。
- 默认路由 :比如访问根路径
/时,重定向到/home或/dashboard。 - 权限控制:未登录用户访问需鉴权的页面时,重定向到登录页。
- 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,根据场景选择即可。
Link 与 a 标签:别用错了
在 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 放最后 :
*路由必须放在所有具体路由之后,否则它会拦截所有请求。 - 用好 hooks :
useParams、useNavigate、useLocation等钩子让路由操作变得无比轻松。 - Suspense 最佳实践 :通常在外层只放一个
<Suspense>,并提供一个全局的 Loading 组件。如果有特殊需求(比如某些区域单独加载),可以再拆分。 - 重定向时注意 replace :避免无限循环,养成使用
replace的好习惯。
React Router 就像应用的导航系统,设计得好,开发体验流畅,用户操作丝滑。希望这份实战指南能帮你轻松驾驭它。