本系列文章将带你系统掌握 React 18+,从 JavaScript 基础到生产级应用开发。这是第四篇,我们将从零到一,用 React Router v6 构建一个完整的权限管理后台路由系统。
引言:当应用需要"多页面"的时候
在前三篇文章中,我们学会了用 React 构建组件、管理状态、处理副作用。但一个真正的 Web 应用,通常由多个"页面"或"视图"组成------首页、产品列表、用户中心、后台管理等。
在传统的多页应用(MPA)中,每次点击链接,浏览器都会向服务器请求一个全新的 HTML 文件,整个页面被刷新。而现代单页应用(SPA)只加载一次 HTML,后续所有"页面切换"都由前端 JavaScript 动态完成。
React 本身不提供路由功能,而 React Router 是 React 生态中最成熟、最流行的路由解决方案 。今天,我们将用 React Router v6 构建一个完整的权限管理后台,涵盖:
- ✅ 基础路由配置(Routes + Route)
- ✅ 嵌套路由与布局(Outlet)
- ✅ 数据加载(Loader + useLoaderData)
- ✅ 表单提交与数据变更(Action + Form)
- ✅ 错误处理(errorElement + useRouteError)
- ✅ 路由守卫(权限控制)
- ✅ 懒加载与代码分割(lazy + Suspense)
- ✅ 动态路由与 URL 参数
第一部分:初识 React Router v6 ------ 三大核心组件
安装与版本说明
React Router 目前最新稳定版为 v6。实际开发中直接安装 react-router-dom 即可(它是 Web 端的封装):
bash
css
npm install react-router-dom@6
三大核心组件
React Router v6 的核心工作流程围绕三个组件展开:
| 组件 | 作用 |
|---|---|
| BrowserRouter | 路由根容器,用 History API 同步 URL 与 UI,包裹整个应用 |
| Routes | 路由"交换机",只渲染第一个路径匹配的 Route |
| Route | 单条路由规则,定义 path 与 element 的映射 |
注意 :v6 用 <Routes> 替换了 v5 的 <Switch>,用 <Navigate> 替换了 <Redirect>。
基础配置
在 main.jsx 中用 BrowserRouter 包裹 App:
jsx
javascript
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
)
然后在 App.jsx 中配置路由:
jsx
javascript
// src/App.jsx
import { Routes, Route, Link } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
import Contact from './pages/Contact'
function App() {
return (
<div>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/contact">联系方式</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
)
}
第二部分:嵌套路由与布局 ------ 构建后台骨架
真实的后台管理应用通常有统一的布局(侧边栏 + 顶部导航 + 内容区)。嵌套路由 正是为此而生------父路由负责布局,子路由在 <Outlet /> 位置渲染。
定义路由配置(对象形式)
v6.4+ 推荐使用 createBrowserRouter 配置路由对象:
jsx
javascript
// src/router/index.jsx
import { createBrowserRouter } from 'react-router-dom'
import DashboardLayout from '../layouts/DashboardLayout'
import Dashboard from '../pages/Dashboard'
import Users from '../pages/Users'
import Orders from '../pages/Orders'
import Settings from '../pages/Settings'
const router = createBrowserRouter([
{
path: '/',
element: <DashboardLayout />,
children: [
{ index: true, element: <Dashboard /> }, // 默认子路由,匹配 /
{ path: 'users', element: <Users /> }, // 匹配 /users
{ path: 'orders', element: <Orders /> }, // 匹配 /orders
{ path: 'settings', element: <Settings /> }, // 匹配 /settings
],
},
])
export default router
布局组件与 Outlet
父组件通过 <Outlet /> 指定子路由渲染的位置:
jsx
javascript
// src/layouts/DashboardLayout.jsx
import { Outlet, Link, NavLink } from 'react-router-dom'
function DashboardLayout() {
return (
<div className="dashboard-layout">
{/* 侧边栏 */}
<aside className="sidebar">
<NavLink to="/" end>仪表盘</NavLink> {/* end 表示精确匹配 */}
<NavLink to="/users">用户管理</NavLink>
<NavLink to="/orders">订单管理</NavLink>
<NavLink to="/settings">系统设置</NavLink>
</aside>
{/* 主内容区 */}
<main className="main-content">
<Outlet /> {/* 子路由组件在这里渲染 */}
</main>
</div>
)
}
💡 小贴士 :index: true 表示该路由是父路由的默认子路由,当路径完全匹配父路径时渲染。NavLink 的 end 属性确保只在精确匹配时高亮。
第三部分:数据加载 ------ Loader 与 useLoaderData
传统方式中,我们通常在 useEffect 里请求数据。v6.4+ 引入了 Loader 机制,在组件渲染之前预先加载数据。
基础用法
jsx
javascript
// src/router/index.jsx
import { createBrowserRouter } from 'react-router-dom'
const router = createBrowserRouter([
{
path: '/users',
loader: async () => {
// 在组件渲染前执行
const response = await fetch('/api/users')
return response.json()
},
element: <Users />,
},
])
在组件中通过 useLoaderData 获取数据:
jsx
javascript
// src/pages/Users.jsx
import { useLoaderData } from 'react-router-dom'
function Users() {
const users = useLoaderData() // 自动获取最近路由的 loader 数据[reference:26]
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}
动态路由参数
通过路径参数(:param)获取动态数据:
jsx
javascript
const router = createBrowserRouter([
{
path: '/users/:userId',
loader: async ({ params }) => {
// params.userId 从 URL 中解析
const response = await fetch(`/api/users/${params.userId}`)
if (!response.ok) {
throw new Response('用户不存在', { status: 404 })
}
return response.json()
},
element: <UserDetail />,
},
])
处理请求参数(URLSearchParams)
Loader 可以获取 request 对象,用于读取查询参数:
jsx
dart
{
path: '/orders',
loader: async ({ request }) => {
const url = new URL(request.url)
const status = url.searchParams.get('status') // ?status=paid
return fetchOrders({ status })
},
element: <Orders />,
}
第四部分:数据变更 ------ Action 与 Form
除了加载数据,我们还需要修改数据 (创建、更新、删除)。Action 是 React Router 提供的表单提交处理机制。
jsx
php
// src/router/index.jsx
import { redirect } from 'react-router-dom'
const router = createBrowserRouter([
{
path: '/users/new',
action: async ({ request }) => {
const formData = await request.formData()
const newUser = {
name: formData.get('name'),
email: formData.get('email'),
role: formData.get('role'),
}
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
headers: { 'Content-Type': 'application/json' },
})
if (!response.ok) {
throw new Response('创建失败', { status: response.status })
}
// 提交成功后重定向到用户列表
return redirect('/users')
},
element: <NewUser />,
},
])
在组件中使用 <Form> 组件(不会触发页面刷新):
jsx
javascript
// src/pages/NewUser.jsx
import { Form } from 'react-router-dom'
function NewUser() {
return (
<Form method="post">
<input name="name" placeholder="姓名" required />
<input name="email" type="email" placeholder="邮箱" required />
<select name="role">
<option value="admin">管理员</option>
<option value="editor">编辑</option>
<option value="viewer">访客</option>
</select>
<button type="submit">创建用户</button>
</Form>
)
}
💡 小贴士 :redirect() 会在 Action 执行完毕后跳转到指定路径。request.formData() 可以获取表单所有字段。
第五部分:错误处理 ------ errorElement
当 Loader、Action 或组件渲染抛出异常时,errorElement 会替代正常的 element 渲染。
全局错误边界与局部错误边界
错误会沿路由树向上冒泡。可以在根路由放置全局错误处理,也可以在特定路由放局部处理:
jsx
javascript
const router = createBrowserRouter([
{
path: '/',
element: <DashboardLayout />,
errorElement: <GlobalErrorBoundary />, // 全局兜底
children: [
{
index: true,
element: <Dashboard />,
},
{
path: 'users/:userId',
loader: async ({ params }) => {
const user = await fetchUser(params.userId)
if (!user) {
throw new Response('用户不存在', { status: 404 }) // 手动抛出[reference:37]
}
return user
},
element: <UserDetail />,
errorElement: <UserErrorBoundary />, // 局部精细处理
},
],
},
])
错误边界组件
使用 useRouteError 和 isRouteErrorResponse 处理不同类型的错误:
jsx
javascript
import { useRouteError, isRouteErrorResponse } from 'react-router-dom'
function UserErrorBoundary() {
const error = useRouteError()
if (isRouteErrorResponse(error)) {
return (
<div className="error-container">
<h1>{error.status}</h1>
<p>{error.statusText || error.data}</p>
{error.status === 404 && <p>请检查用户 ID 是否正确</p>}
{error.status === 403 && <p>您没有权限查看此用户</p>}
</div>
)
}
return (
<div className="error-container">
<h1>出错了</h1>
<p>{error?.message || '未知错误'}</p>
</div>
)
}
第六部分:路由守卫 ------ 权限控制
React Router v6 本身没有内置路由守卫,但我们可以通过高阶组件(HOC) 或自定义布局组件实现。
方案一:受保护路由组件(推荐)
jsx
javascript
// src/components/ProtectedRoute.jsx
import { Navigate, Outlet } from 'react-router-dom'
function ProtectedRoute({ allowedRoles = [] }) {
const { user } = useAuth() // 自定义 Hook,从 Context 获取用户信息
if (!user) {
return <Navigate to="/login" replace /> // 未登录跳转登录页[reference:42]
}
if (allowedRoles.length && !allowedRoles.includes(user.role)) {
return <Navigate to="/403" replace /> // 无权限跳转 403
}
return <Outlet /> // 有权限则渲染子路由
}
在路由配置中使用
jsx
php
const router = createBrowserRouter([
{
path: '/',
element: <DashboardLayout />,
children: [
// 公开路由
{ path: 'login', element: <Login /> },
{ path: '403', element: <Forbidden /> },
// 受保护路由(需要登录)
{
element: <ProtectedRoute />,
children: [
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'profile', element: <Profile /> },
],
},
// 受保护路由(需要管理员权限)
{
element: <ProtectedRoute allowedRoles={['admin']} />,
children: [
{ path: 'users', element: <Users /> },
{ path: 'settings', element: <Settings /> },
],
},
],
},
])
方案二:AuthProvider + Context
用 Context 全局管理认证状态:
jsx
javascript
// src/contexts/AuthContext.jsx
import { createContext, useContext, useState } from 'react'
const AuthContext = createContext(null)
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const login = (userData) => setUser(userData)
const logout = () => setUser(null)
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
return useContext(AuthContext)
}
第七部分:懒加载与代码分割 ------ 性能优化
随着应用膨胀,所有代码打包进一个 bundle 会导致首屏加载缓慢。懒加载让每个页面组件独立打包,按需加载。
基础用法
jsx
javascript
import { lazy, Suspense } from 'react-components'
// 静态引入(不推荐用于路由组件)[reference:46]
// import Users from './pages/Users'
// 动态引入(推荐)[reference:47]
const Users = lazy(() => import('./pages/Users'))
const Orders = lazy(() => import('./pages/Orders'))
const Settings = lazy(() => import('./pages/Settings'))
在路由中使用
jsx
javascript
import { Suspense } from 'react'
import { createBrowserRouter } from 'react-router-dom'
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Users = lazy(() => import('./pages/Users'))
const Orders = lazy(() => import('./pages/Orders'))
const router = createBrowserRouter([
{
path: '/',
element: <DashboardLayout />,
children: [
{
index: true,
element: (
<Suspense fallback={<div className="loading-spinner">加载中...</div>}>
<Dashboard />
</Suspense>
),
},
{
path: 'users',
element: (
<Suspense fallback={<div className="loading-spinner">加载中...</div>}>
<Users />
</Suspense>
),
},
// ... 更多懒加载路由
],
},
])
💡 小贴士 :lazy() 需要配合 <Suspense> 使用。Webpack 或 Vite 会自动将每个 lazy 组件分离成独立的 chunk。
第八部分:完整实战 ------ 权限管理后台
现在,我们把所有知识整合成一个完整的权限管理后台。
项目结构
text
bash
src/
├── main.jsx # 入口,BrowserRouter
├── App.jsx # RouterProvider
├── router/
│ └── index.jsx # 路由配置(createBrowserRouter)
├── layouts/
│ └── DashboardLayout.jsx # 后台布局(含 Outlet)
├── pages/
│ ├── Login.jsx
│ ├── Dashboard.jsx
│ ├── Users.jsx
│ ├── UserDetail.jsx
│ ├── NewUser.jsx
│ └── Forbidden.jsx
├── components/
│ └── ProtectedRoute.jsx # 路由守卫
├── contexts/
│ └── AuthContext.jsx # 认证状态
└── hooks/
└── useAuth.js # 认证 Hook
完整路由配置
jsx
javascript
// src/router/index.jsx
import { createBrowserRouter, Navigate } from 'react-router-dom'
import { lazy, Suspense } from 'react'
import DashboardLayout from '../layouts/DashboardLayout'
import ProtectedRoute from '../components/ProtectedRoute'
import Login from '../pages/Login'
import Forbidden from '../pages/Forbidden'
import GlobalErrorBoundary from '../components/GlobalErrorBoundary'
// 懒加载页面组件
const Dashboard = lazy(() => import('../pages/Dashboard'))
const Users = lazy(() => import('../pages/Users'))
const UserDetail = lazy(() => import('../pages/UserDetail'))
const NewUser = lazy(() => import('../pages/NewUser'))
const Orders = lazy(() => import('../pages/Orders'))
const Settings = lazy(() => import('../pages/Settings'))
// 懒加载包裹组件
const LazyPage = ({ children }) => (
<Suspense fallback={<div className="loading-spinner">加载中...</div>}>
{children}
</Suspense>
)
const router = createBrowserRouter([
{
path: '/login',
element: <Login />,
errorElement: <GlobalErrorBoundary />,
},
{
path: '/403',
element: <Forbidden />,
},
{
path: '/',
element: <DashboardLayout />,
errorElement: <GlobalErrorBoundary />,
children: [
// 所有路由默认需要登录
{
element: <ProtectedRoute />,
children: [
{
index: true,
element: <LazyPage><Dashboard /></LazyPage>,
},
{
path: 'profile',
element: <LazyPage><Profile /></LazyPage>,
},
],
},
// 管理员专用路由
{
element: <ProtectedRoute allowedRoles={['admin']} />,
children: [
{
path: 'users',
loader: async () => {
const res = await fetch('/api/users')
if (!res.ok) throw new Response('加载失败', { status: res.status })
return res.json()
},
element: <LazyPage><Users /></LazyPage>,
children: [
{
path: ':userId',
loader: async ({ params }) => {
const res = await fetch(`/api/users/${params.userId}`)
if (!res.ok) throw new Response('用户不存在', { status: 404 })
return res.json()
},
element: <LazyPage><UserDetail /></LazyPage>,
errorElement: <UserErrorBoundary />,
},
],
},
{
path: 'users/new',
action: async ({ request }) => {
const formData = await request.formData()
// 创建用户逻辑...
return redirect('/users')
},
element: <LazyPage><NewUser /></LazyPage>,
},
{
path: 'orders',
loader: async ({ request }) => {
const url = new URL(request.url)
const status = url.searchParams.get('status')
return fetchOrders({ status })
},
element: <LazyPage><Orders /></LazyPage>,
},
{
path: 'settings',
element: <LazyPage><Settings /></LazyPage>,
},
],
},
// 404 兜底
{
path: '*',
element: <Navigate to="/404" replace />,
},
],
},
])
export default router
App 入口
jsx
javascript
// src/App.jsx
import { RouterProvider } from 'react-router-dom'
import { AuthProvider } from './contexts/AuthContext'
import router from './router'
function App() {
return (
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
)
}
总结与下一期预告
今天我们完成了一次完整的 React Router v6 实战之旅,核心要点:
| 知识点 | 核心 API |
|---|---|
| 路由配置 | createBrowserRouter + Route 对象 |
| 嵌套路由 | children + <Outlet /> |
| 数据加载 | loader + useLoaderData |
| 数据变更 | action + <Form> + redirect |
| 错误处理 | errorElement + useRouteError |
| 路由守卫 | 自定义 ProtectedRoute + Navigate |
| 懒加载 | React.lazy + <Suspense> |
| 导航 | <Link> / <NavLink> + useNavigate |
| 参数读取 | useParams + useSearchParams |
React Router v6 让路由管理变得声明式、类型安全、性能友好。掌握这些技巧,你就能轻松驾驭任何规模的 React 应用。