React 进阶指南:React Router v6 完全实战(第四篇)

本系列文章将带你系统掌握 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 表示该路由是父路由的默认子路由,当路径完全匹配父路径时渲染。NavLinkend 属性确保只在精确匹配时高亮。


第三部分:数据加载 ------ 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 />,  // 局部精细处理
      },
    ],
  },
])

错误边界组件

使用 useRouteErrorisRouteErrorResponse 处理不同类型的错误:

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 应用。

相关推荐
YFF菲菲兔21 小时前
调度系统和调和系统的桥梁
react.js
YFF菲菲兔1 天前
commitRoot 源码解析
react.js
光影少年2 天前
react批量更新、同步/异步更新场景
前端·react.js·掘金·金石计划
YFF菲菲兔2 天前
completeRoot 源码解析
react.js
光影少年3 天前
React 合成事件机制、和原生事件区别、事件冒泡阻止
前端·react.js·掘金·金石计划
YFF菲菲兔3 天前
finishConcurrentRender 源码解析
react.js
YFF菲菲兔3 天前
reconcileChildren 源码解析
react.js
还有多久拿退休金4 天前
Ant Design Tree 搜索定位避坑指南:虚拟滚动下如何实现高亮与精准定位
前端·react.js
光影少年4 天前
react 原理与进阶
前端·react.js·掘金·金石计划