React Router 路由:单页应用导航与守卫
React Router 路由:单页应用导航与守卫
在现代 Web 开发中,单页应用(SPA)已成为主流。React Router 是 React 生态中最流行的路由库,它让你无需刷新页面即可在不同视图间平滑切换,同时支持动态参数、嵌套布局和路由守卫。本教程从零开始,带你掌握 React Router v6 的核心用法,并实现受保护的路由。
为什么需要 React Router
传统的多页应用每次跳转都会向服务器请求全新页面,导致白屏和状态丢失。React Router 通过客户端路由,只替换页面中变化的组件,保持应用状态,带来近似原生 App 的流畅体验。它基于浏览器 History API,URL 变化时仅重新渲染匹配的组件树。
安装与快速上手
首先,通过 npm 或 yarn 安装 React Router v6:
npm install react-router-dom
# 或
yarn add react-router-dom
在你的应用入口文件中,引入并配置 BrowserRouter,它作为整个路由系统的容器:
import { BrowserRouter } from 'react-router-dom';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
接下来,在 App 组件中使用 Routes 和 Route 定义页面映射:
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
);
}
<Routes> 会遍历其子 <Route>,仅渲染第一个匹配的 element。没有匹配时,可以添加一个 * 通配符路由作为 404 页面。
声明式导航与链接
应用内跳转绝不能用 <a> 标签,因为它会触发完整页面刷新。React Router 提供三种内置导航方式:
1. Link 组件
生成标准的无障碍链接,点击时拦截默认行为,执行客户端路由:
import { Link } from 'react-router-dom';
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
to 属性可以是字符串,也可以是对象,用于携带查询参数或 hash:
<Link to={{ pathname: '/products', search: '?category=books' }}>书籍</Link>
2. NavLink 组件
NavLink 继承自 Link,但多了一个激活态样式。当 to 与当前 URL 匹配时,会自动添加 active 类名,也可以自定义样式或类名:
<NavLink
to="/dashboard"
className={({ isActive }) => isActive ? 'text-blue-600 font-bold' : 'text-gray-600'}
>
仪表盘
</NavLink>
end 属性可禁用祖先路由的匹配,避免嵌套路由时父链接持续高亮。
3. 编程式导航:useNavigate
在事件处理函数或副作用中跳转,使用 useNavigate 钩子:
import { useNavigate } from 'react-router-dom';
function LoginButton() {
const navigate = useNavigate();
const handleLogin = () => {
// 登录逻辑...
navigate('/dashboard', { replace: true });
};
return <button onClick={handleLogin}>登录</button>;
}
navigate 还支持传递 state 数据,并通过目标组件的 useLocation 获取:
navigate('/product/123', { state: { fromCatalog: true } });
动态路由与 URL 参数
许多页面需要根据 ID、slug 等参数改变内容。React Router 使用 :paramName 占位符定义动态路径:
<Route path="/users/:userId" element={<UserProfile />} />
在 UserProfile 组件内,通过 useParams 提取参数:
import { useParams } from 'react-router-dom';
function UserProfile() {
const { userId } = useParams();
// 使用 userId 获取数据...
return <div>用户 ID:{userId}</div>;
}
多个参数或无限制子级也可支持:
<Route path="/files/*" element={<FileViewer />} />
// useParams 返回 { "*": "path/to/file" }
嵌套路由与布局
嵌套路由让页面共享布局(如侧边栏、顶栏),避免重复代码。在父组件中使用 <Outlet /> 作为子路由的渲染出口。
定义嵌套路由:
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
DashboardLayout 组件:
import { Outlet } from 'react-router-dom';
import Sidebar from './Sidebar';
function DashboardLayout() {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">
<Outlet /> {/* 子路由内容在此显示 */}
</main>
</div>
);
}
index 路由匹配父路由的默认子页面(即 /dashboard 精确路径)。
路由守卫:保护私密页面
路由守卫控制用户的访问权限,例如未登录用户跳转到登录页。React Router v6 没有内置的守卫组件,但可以通过封装一个 ProtectedRoute 高阶组件轻松实现。
常见模式:
import { Navigate, useLocation } from 'react-router-dom';
function ProtectedRoute({ children }) {
const isAuthenticated = useAuth(); // 你的认证钩子,返回 true/false
const location = useLocation();
if (!isAuthenticated) {
// 将用户重定向到登录页,并记录来源路径以便登陆后返回
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
在路由配置中使用:
<Route path="/admin" element={
<ProtectedRoute>
<AdminPanel />
</ProtectedRoute>
} />
如果需要更复杂的权限(如角色检查),可以在 ProtectedRoute 中扩展逻辑,返回不同内容或 <Navigate />。
基于角色的守卫示例
function RoleGuard({ allowedRoles, children }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" />;
if (!allowedRoles.includes(user.role)) return <Navigate to="/unauthorized" />;
return children;
}
<Route path="/admin" element={
<RoleGuard allowedRoles={['admin', 'editor']}>
<AdminPanel />
</RoleGuard>
} />
代码分割与路由懒加载
大型应用中,按路由拆分代码可以显著提升首屏加载速度。React 的 lazy 与 Suspense 配合路由组件即可:
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
);
}
这样,每个页面仅在首次访问时加载对应的 JS 包。
处理查询参数与 state
除路径参数外,常需要获取查询字符串 ?key=value 或跨页面传递状态。
读取查询参数:useSearchParams
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get('q') || '';
const updateKeyword = (newValue) => {
setSearchParams({ q: newValue });
};
// ...
}
useSearchParams 类似 useState,直接操作即可同步 URL。
路由状态传递
通过 navigate 或 Link 的 state 属性传递不显示在 URL 中的数据:
// 发送端
navigate('/product', { state: { category: 'electronics' } });
// 接收端
import { useLocation } from 'react-router-dom';
const location = useLocation();
const category = location.state?.category;
注意:state 在浏览器刷新后默认保留(基于 history.state),但若用户手动清空浏览器历史,则会丢失。
总结
React Router v6 用声明式组件和钩子,让单页应用的路由管理清晰而强大。掌握基础路由配置、动态参数、嵌套布局和守卫后,你已能构建任意复杂度的导航逻辑。关键实践:
- 始终使用
Link或useNavigate,避免硬刷新。 - 利用
Outlet实现布局复用。 - 用
ProtectedRoute+Navigate实现认证守卫。 - 懒加载路由以优化性能。
现在,你可以开始在项目中构建流畅、安全的导航体验了。