告别页面白屏,拥抱 SPA 的流畅体验。本文带你从零搭建一个包含嵌套路由、动态路由、懒加载与登录鉴权的完整 React 项目。
写在前面:为什么我们需要前端路由?
在早期的 Web 开发中,路由是由后端控制的。每次点击链接,浏览器都会向服务器发送请求,服务器返回新的 HTML 页面。这个过程会导致页面刷新,出现"白屏"现象,用户体验非常不友好。
随着 前后端分离 和 SPA(单页应用) 架构的兴起,前端路由 应运而生。它的核心思想是:一切皆资源(RESTful 理念) ,通过改变 URL 来切换页面,但不刷新浏览器。
- Hash 模式 (HashRouter) :利用 URL 的
hash(#)部分。当hash改变时,页面不会重新加载,通过监听hashchange事件来渲染对应组件。 - History 模式 (BrowserRouter) :利用 HTML5 的 History API(
pushState,replaceState),使 URL 更加美观(不带#),但需要后端配置做兜底。
在 React 生态中,react-router-dom 是官方的路由解决方案。今天,我们将通过一个实战 Demo,把路由的核心知识点一次讲透。
一、项目初始化与架构
1. 创建项目与安装依赖
bash
bash
# 使用 Vite 快速创建 React 项目
npm create vite@latest react-router-demo -- --template react
cd react-router-demo
# 安装路由核心库
npm i react-router-dom
2. 目录结构解析
为了更好的维护性,我们按功能模块划分 pages 目录:
text
bash
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ └── Navigation.jsx
├── pages/
│ ├── Home/
│ ├── About/
│ ├── Login/
│ ├── Pay/
│ ├── Products/
│ │ ├── Detail/ # 动态路由参数
│ │ └── New/ # 嵌套路由
│ ├── User/
│ │ └── UserProfile/
│ └── NotFound/
├── App.jsx # 路由配置中枢
├── ProtectRoute.jsx # 高阶鉴权组件
└── main.jsx
二、路由基础配置与懒加载优化
在 App.jsx 中,我们使用 lazy + Suspense 实现代码分割。这意味着只有用户访问该页面时,对应的 JS 文件才会被加载,大大提升了首屏加载速度。
jsx
javascript
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
// 懒加载组件:优化性能,非当前路由不加载
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'));
function App() {
return (
<Router>
<Suspense fallback={<div className="loading">Loading...</div>}>
<Navigation /> {/* 导航栏组件 */}
<div id="container">
<Routes>
{/* 基础路由:路径与组件一一对应 */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* 动态路由:使用 :id 匹配任意值,通过 useParams 获取 */}
<Route path="/user/:id" element={<UserProfile />} />
{/* 嵌套路由:父组件定义布局,子组件通过 <Outlet /> 渲染 */}
<Route path="/products" element={<Products />}>
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
{/* 重定向:旧路径导航到新路径 */}
<Route path="/old-path" element={<Navigate replace to="/new-path" />} />
{/* 鉴权路由:保护 /pay 路径 */}
<Route
path="/pay"
element={
<ProtectRoute>
<Pay />
</ProtectRoute>
}
/>
<Route path="/login" element={<Login />} />
{/* 404 兜底:匹配所有未定义的路径,必须放在最后 */}
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</Suspense>
</Router>
);
}
export default App;
注释解读:
<Routes>是路由表,它会根据当前 URL 匹配唯一的<Route>。element接收组件实例(带标签),而非传入构造函数。*通配符用于捕获所有未匹配的路径,做 404 页面。
三、核心功能深度解析
1. 嵌套路由与 Outlet
在 Products 组件中,我们并没有直接展示详情或新增表单,而是通过 <Outlet /> 占位。当 URL 变为 /products/123 或 /products/new 时,子路由组件会渲染在这个出口位置。
jsx
javascript
// src/pages/Products/index.jsx
import { Outlet } from 'react-router-dom';
const Products = () => {
return (
<>
<h1>产品列表</h1>
{/* 二级路由渲染出口:这里会显示 ProductDetail 或 NewProduct */}
<Outlet />
</>
);
};
export default Products;
2. 动态路由与 useParams
/user/:id 这种格式叫动态路由。在子组件中,我们可以使用 useParams Hook 拿到 URL 上的具体值。
jsx
javascript
// src/pages/User/UserProfile/index.jsx
import { useParams } from 'react-router-dom';
function UserProfile() {
// 从 URL 中提取参数,hook 思想:召之即来
let { id } = useParams();
console.log('当前用户ID:', id);
return <h2>UserProfile: {id}</h2>;
}
export default UserProfile;
3. Navigate 与 useNavigate 编程式跳转
- 声明式重定向 :在路由表中使用
<Navigate to="/new-path" replace />。 - 编程式跳转 :在事件处理函数中使用
useNavigate()。
以 404 页面为例,3 秒后自动跳回首页:
jsx
javascript
// src/pages/NotFound/index.jsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
const NotFound = () => {
let navigate = useNavigate();
useEffect(() => {
const timer = setTimeout(() => {
navigate('/'); // 等价于 window.location.href,但不会刷新页面
}, 3000);
return () => clearTimeout(timer);
}, [navigate]);
return <>Not Found,3秒后返回首页</>;
};
export default NotFound;
4. 导航栏组件:Link 的使用
不要使用 <a> 标签,因为 <a> 会触发浏览器刷新。使用 react-router-dom 提供的 <Link> 或 <NavLink> 来实现无刷新跳转。
jsx
javascript
// src/components/Navigation.jsx
import { Link } from 'react-router-dom';
function Navigation() {
return (
<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>
<li><Link to="/products/new">新增产品</Link></li>
<li><Link to="/pay">支付(需登录)</Link></li>
</ul>
</nav>
);
}
export default Navigation;
四、鉴权路由(核心实战)
前端鉴权的本质是状态管理 + 条件渲染 。我们利用 localStorage 模拟登录态,结合 Navigate 实现路由拦截。
1. 保护层组件 ProtectRoute
这是一个典型的高阶组件(HOC) ,它接收 children 作为要保护的页面。利用 props.children 的定制性,我们可以像弹窗组件一样嵌套任意内容。
jsx
javascript
// src/ProtectRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
const ProtectRoute = ({ children }) => {
const location = useLocation(); // 获取当前请求的路径
// 1. 鉴权逻辑:从 localStorage 读取登录状态
const isLogin = localStorage.getItem('isLogin') === 'true';
console.log('鉴权校验:', isLogin);
if (!isLogin) {
// 2. 拦截:重定向到登录页,并携带当前路径来源 (from)
// state 传参:登录后直接跳回来,提升用户体验
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
// 3. 放行:渲染被包裹的页面组件
return <div>{children}</div>;
};
export default ProtectRoute;
2. 登录页逻辑:获取来源并重定向
登录页通过 useLocation 获取 state 中的 from 字段,登录成功后跳转回原页面。注意使用 replace: true 替换历史记录,防止用户通过"后退"按钮回到登录页(小白用户可能会困惑)。
jsx
ini
// src/pages/Login/index.jsx
import { useNavigate, useLocation } from 'react-router-dom';
const Login = () => {
const navigate = useNavigate();
const location = useLocation();
// 从 state 中读取来源,若无则默认跳转首页
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') {
// 1. 写入登录凭证
localStorage.setItem('isLogin', 'true');
// 2. 跳转回来源页,并替换历史记录,防止返回登录页
navigate(from, { replace: true });
} else {
alert('用户名或密码错误!');
}
}
return (
<form onSubmit={handleSubmit}>
<h1>登录</h1>
<input name="username" placeholder="用户名: admin" required />
<input name="password" type="password" placeholder="密码: 123456" required />
<button type="submit">登录</button>
</form>
);
};
export default Login;
五、细节点与最佳实践
-
children的妙用 :ProtectRoute和<Model>弹窗组件都利用了props.children,这使得组件的定制性极强,可以包裹任意复杂的 UI 结构。 -
状态管理:
- 无状态:HTTP 协议本身是无状态的。
- 有状态 :我们通过
LocalStorage(存isLogin)和请求头携带Authorization Token来模拟状态。 - 注意 :实际生产环境中,登录态通常由后端颁发的 JWT Token 管理,存储在
Cookie或LocalStorage中。
-
路由传参方式:
- 动态路由参数 :
/user/:id,使用useParams获取。 - 查询参数 (Query) :
/pay?orderId=123,使用useSearchParams获取。 - 状态参数 (State) :
Navigate的state属性,使用useLocation获取(如上文的from路径)。
- 动态路由参数 :
总结
至此,我们完成了一个包含懒加载、嵌套路由、动态路由、编程式导航、鉴权拦截、登录重定向 以及 404 兜底的完整 React 路由项目。
掌握 React Router,不仅仅是学会使用 API,更是理解了单页应用架构的核心思想。希望这篇文章能帮你打通任督二脉,在实际项目中写出更优雅、更健壮的路由代码。