React09:React Router 路由

本文系统讲解 React Router v6/v7 的核心用法,涵盖组件式与对象式路由配置、Layout+Outlet 布局嵌套、动态路由与 404 处理、Link/NavLink 编程式导航、React.lazy+Suspense 懒加载,以及基于守卫组件实现的登录拦截方案。强调企业级最佳实践,如路由表集中管理、查询参数状态持久化、权限守卫封装和常见避坑指南。

第 09 课 · React Router 路由

🎯 本课目标

  • 会写两种路由表写法(组件式 <Routes> 与对象式 createBrowserRouter),知道企业主流是后者
  • 会配置布局路由 + <Outlet>,对照 <router-view> 理解嵌套与 index route
  • 会用 :id 动态路由 + useParams,以及 path="*" 做 404 兜底
  • 会用 <Link> / <NavLink>(active 高亮)、useNavigate(push / replace / back)和 useSearchParams
  • 会用 React.lazy + <Suspense> 做路由懒加载
  • 会封装 <RequireAuth> 守卫组件实现登录拦截——React Router 没有 beforeEach

1. 两种写法:组件式 vs 对象式

Vue 里你这样写

// router/index.ts
const routes = [
  { path: '/', component: Layout, children: [
    { path: '', component: UserList },
    { path: 'users/:id', component: UserDetail },
  ] },
  { path: '/:pathMatch(.*)*', component: NotFound },
]

React 里这样写

写法一:组件式(小项目、本课 Demo 用它,直观)

import { MemoryRouter, Route, Routes } from 'react-router-dom'
// 真实项目:import { BrowserRouter, Route, Routes } from 'react-router-dom'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        {/* 布局路由:Layout 里的 <Outlet /> 负责渲染子路由 */}
        <Route path="/" element={<Layout />}>
          <Route index element={<UserList />} />
          <Route path="users/:id" element={<UserDetail />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  )
}

写法二:对象式 createBrowserRouter(企业 v6+ 主流,路由集中可配置)

import { createBrowserRouter, RouterProvider } from 'react-router-dom'

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <UserList /> },
      { path: 'users/:id', element: <UserDetail /> },
      { path: 'about', lazy: () => import('./pages/About') },
    ],
  },
  { path: '*', element: <NotFound /> },
])

function App() {
  return <RouterProvider router={router} />
}

关键差异与注意点

  • React Router 的「路由表」映射的是组件(element / Component),vue-router 映射的也是组件,心智几乎一样;children 嵌套、index(对应 path: '')、* 兜底完全对得上。
  • 两种写法渲染结果一样,只是组织形式不同;一个项目里选一种用到底,企业里见到 createBrowserRouter 概率最高。
  • v7(本课程安装的版本)沿用 v6 的 API 风格,老项目里也可能看到 v5 的 <Switch component> 写法——认出来即可,不必学。

2. 布局路由与嵌套:Layout + Outlet

Vue 里你这样写

<!-- Layout.vue -->
<template>
  <nav>...</nav>
  <router-view />  <!-- 子路由渲染在这里 -->
</template>

React 里这样写

import { NavLink, Outlet } from 'react-router-dom'

function Layout() {
  return (
    <div style={{ display: 'flex' }}>
      <nav>
        <NavLink to="/" end>用户列表</NavLink>
        <NavLink to="/about">关于</NavLink>
      </nav>
      {/* 子路由渲染在这里,就是 <router-view /> */}
      <main><Outlet /></main>
    </div>
  )
}

关键差异与注意点

  • <Route path="/" element={<Layout />}> 的 children 渲染进 Layout 里的 <Outlet />;切换子路由时 Layout 不重新挂载,侧边栏、面包屑、滚动位置天然保留。
  • index 路由 = 父路径上的默认页(对应 vue-router 的 path: '')。
  • end 属性:<NavLink to="/" end> 让 /users/1 不再把 / 标记为 active(对应 exact-active-class 的行为),根路径导航必须加。

3. 动态路由、404 与参数获取

import { useParams } from 'react-router-dom'

// 路由表:<Route path="users/:id" element={<UserDetail />} />
function UserDetail() {
  const { id } = useParams<{ id: string }>()  // 对照 route.params,注意:值永远是 string
  const user = USERS.find((u) => u.id === Number(id)) // 需要 number 就自己转
  if (!user) return <NotFound />
  return <div>{user.name}</div>
}

要点:

  • 动态段写法 :id 与 vue-router 相同;取参从「route.params.id 响应式对象」变成「useParams() 返回的本次渲染快照」。
  • 参数变化时组件不会重新挂载,只是重新执行:/users/1 → /users/2 同一个组件实例,useParams 返回新值(与 vue-router 组件复用行为一致)。
  • 404:<Route path="*" element={<NotFound />} /> 放在路由表最后,对应 vue-router 的 /:pathMatch(.*)*。

4. Link、NavLink 与编程式导航

Vue 里你这样写

<router-link :to="{ path: '/users/1' }">详情</router-link>
<script setup>
const route = useRoute(); const router = useRouter()
router.push('/users/1'); router.replace('/users/1'); router.back()
</script>

React 里这样写

import { Link, useNavigate, useSearchParams } from 'react-router-dom'

function SearchList() {
  const navigate = useNavigate()
  const [searchParams, setSearchParams] = useSearchParams()
  const dept = searchParams.get('dept') ?? '全部'   // 对照 route.query
  const keyword = searchParams.get('kw') ?? ''

  return (
    <>
      {/* <Link> 就是 <router-link>:改成地址栏但不重新加载页面 */}
      <Link to={`/users/${user.id}`}>{user.name}</Link>

      {/* useNavigate 就是 useRouter:push 是默认行为,replace 可选 */}
      <button onClick={() => navigate('/help')}>push 跳转</button>
      <button onClick={() => navigate('/help', { replace: true })}>replace 跳转</button>
      <button onClick={() => navigate(-1)}>后退</button>

      {/* 改查询参数:setSearchParams 直接写入 URL,组件自动重渲染 */}
      <input value={keyword} onChange={(e) => setSearchParams({ dept, kw: e.target.value })} />
    </>
  )
}

关键差异与注意点

  • navigate() 默认 push,{ replace: true } 替换当前记录;navigate(-1) 后退、navigate(1) 前进,数字语义与 router.go() 一致。
  • useSearchParams 返回 [searchParams, setSearchParams];searchParams.get('kw') 读,setSearchParams({ kw }) 或 setSearchParams(prev => ...) 写。查询参数写进 URL 的最大好处:筛选状态可刷新、可分享、可后退,这是中后台列表页的标配做法。
  • 为什么用 <Link> 不用 <a>:<a href> 会触发浏览器整页加载,React 应用状态全部丢失、JS 重新执行;<Link> 只更新地址栏并让路由重新匹配。同理 <NavLink> 额外提供 active 状态,用于导航高亮。

5. 路由懒加载:React.lazy + Suspense

Vue 里你这样写

{ path: '/dashboard', component: () => import('./Dashboard.vue') }

React 里这样写

import { lazy, Suspense } from 'react'

// 打包时自动拆成独立 chunk,首次访问才下载;组件文件必须默认导出
const Dashboard = lazy(() => import('./pages/Dashboard'))

function App() {
  return (
    // vue-router 自动处理了加载态,React 需要自己包一层 Suspense 指定占位 UI
    <Suspense fallback={<div>页面加载中…</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </Suspense>
  )
}

要点:

  • lazy() 的动态 import() 必须指向默认导出的模块,命名导出要用 .then(m => ({ default: m.Dashboard })) 转一层。
  • fallback 是首次进入该路由前的占位,企业里一般放骨架屏;加载完成后 chunk 会被缓存,第二次进入不再出现 loading。
  • Demo 3 里「清缓存重新演示」按钮是为了让 loading 可重复观察(lazy 有模块级缓存)。

6. 登录守卫:React Router 没有 beforeEach

Vue 里你这样写

router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isLogin()) return '/login'
})

React 里这样写(企业最常见做法:守卫组件)

import { Navigate, useLocation } from 'react-router-dom'
import type { ReactNode } from 'react'

function RequireAuth({ isLogin, children }: { isLogin: boolean; children: ReactNode }) {
  const location = useLocation()
  if (!isLogin) {
    // 未登录 → 重定向 /login;replace 不留历史;state 记住来源页
    return <Navigate to="/login" replace state={{ from: location.pathname }} />
  }
  return <>{children}</>
}

// 使用:受保护路由用守卫组件包一层
<Route
  path="/admin"
  element={
    <RequireAuth isLogin={isLogin}>
      <AdminPage />
    </RequireAuth>
  }
/>

登录页读回来源页:

function LoginPage() {
  const navigate = useNavigate()
  const location = useLocation()
  const from = (location.state as { from?: string } | null)?.from ?? '/'
  return <button onClick={() => { doLogin(); navigate(from, { replace: true }) }}>登录</button>
}

关键差异与注意点

  • 这是两套框架思路差异最大的一处:vue-router 在「导航生命周期」上做拦截(全局守卫),React Router 把它还原成「组件渲染逻辑」——条件不满足就渲染 <Navigate>。
  • 优点:守卫就是普通组件,可以组合、可以按路由粒度随意套(<RequireAuth>、<RequireRole roles={['admin']}>);缺点:没有集中配置点,权限矩阵大了要自己写一个「受保护路由工厂函数」统一生成。
  • <Navigate to="/login" replace /> 渲染即跳转,replace 避免后退又回到被拦截页。
  • 登录态本身建议放全局状态(第 10 课 Zustand),守卫组件只负责「读状态 + 重定向」。

📊 Vue ↔ React 对照速查表

你熟悉的(vue-router)React Router一句话差异
routes 数组 + createRoutercreateBrowserRouter 数组 + RouterProvider同为配置式路由表,企业主流
<router-view /><Outlet />嵌套子路由的渲染出口
component: () => import()lazy(() => import()) + <Suspense>React 要自己写加载占位
route.paramsuseParams()值都是 string,同样不会触发重新挂载
route.queryuseSearchParams()React 还给了直接写回的 setter
router.push / replace / backnavigate(to) / navigate(to, { replace }) / navigate(-1)语义一致,API 名不同
<router-link> / active-class<Link> / <NavLink> + active 状态NavLink 用函数式 className 或 style 做高亮
/:pathMatch(.*)*path="*"404 兜底,同样放最后
全局守卫 beforeEach无,封装 <RequireAuth> 守卫组件思路从「拦截导航」变成「条件渲染」
router/index.ts 目录src/router/index.tsx(或路由表集中文件)组织方式一致

🏢 企业实战要点

  • 目录组织:src/router/index.tsx 放路由表(对象式),src/pages/(或 src/views/)放页面组件;布局组件单独 src/layouts/MainLayout.tsx。路由表里只写 path + element + lazy,不写业务。
  • 守卫组件模板要沉淀成团队公共组件:<RequireAuth>(登录)、<RequireRole roles={...}>(权限点)、<RequirePerms>(接口级权限),全部走同一个 useAuth() Hook 读状态,避免各页面各写一套。
  • 为什么用 <Link> 不用 <a>(code review 高频意见):<a> 整页刷新会丢掉全部应用状态、重新下载执行 JS;确需外链才用 <a>,站内跳转一律 <Link> / navigate。
  • 列表页把筛选条件写进 query(useSearchParams):刷新不丢、可分享、浏览器后退可回退筛选,比放 useState 强一个档次。
  • 懒加载按页面粒度拆,不要按小组件拆:一个页面一个 chunk;同时记得给 Suspense 配骨架屏而不是白屏。
  • key 的小技巧:<Route element={<UserDetail />} /> 在 /users/1 → /users/2 时组件复用,如果页面里有「进入时滚到顶部」等一次性逻辑,需要监听 useParams 的变化,或给组件加 key={id} 强制重建。
  • 真实坑 1:NavLink to="/" 忘加 end,导致所有页面都把「首页」高亮了。
  • 真实坑 2:useParams 拿到的 id 是 string,直接 USERS.find(u => u.id === id) 永远找不到——用 Number(id) 转一次。
  • 真实坑 3:404 路由写在了具体路由前面(或没写),用户访问任意错误路径白屏;path="*" 永远放最后。

💼 面试高频

Q1:React Router 和 vue-router 最大的差异?(高频)

架构思路:vue-router 提供全局导航守卫(beforeEach / afterEach / 路由独享守卫),在导航生命周期里统一拦截;React Router 没有全局守卫,把权限、重定向都还原成组件逻辑——典型做法是封装 <RequireAuth> 守卫组件包住受保护路由,内部用 <Navigate> 重定向。此外 React Router 的嵌套出口叫 <Outlet>,query 用 useSearchParams,编程导航用 useNavigate,能力上无差别。

Q2:动态路由参数怎么获取?

路由表里写 path="users/:id",组件里 const { id } = useParams()。注意两点:值永远是字符串,比较数字要先转换;同组件在参数变化时只重新执行、不重新挂载,依赖参数的一次性逻辑要么监听参数变化,要么用 key 强制重建。

Q3:如何做权限守卫 / 登录拦截?

没有 beforeEach,主流做法是守卫组件:RequireAuth 内部读登录态(全局 store 或 Context),未登录就 return <Navigate to="/login" replace state={{ from: pathname }} />,已登录渲染 children;登录页从 location.state.from 读出来源页,登录成功后跳回。复杂权限再叠加 <RequireRole>、路由表集中配置 meta 的方案。

Q4:Link 和 a 标签的区别?

<a href> 触发浏览器整页加载:重新请求 HTML、重新执行 JS、所有 React 状态丢失;<Link> 拦截默认行为,只更新地址栏并让 React Router 重新匹配渲染,实现 SPA 的无刷新导航。<NavLink> 在 <Link> 基础上多提供「当前路由是否匹配」的状态用于高亮。

Q5:路由懒加载怎么做?

const Dashboard = lazy(() => import('./pages/Dashboard')),配合 <Suspense fallback={<骨架屏/>}> 包住路由出口。构建工具会把 import() 指向的模块拆成独立 chunk,首次进入该路由才下载;组件文件必须默认导出。对应 vue-router 的 component: () => import('./Dashboard.vue'),区别是要自己指定加载占位。

📌 小结 + 下节预告

  • 两种路由表写法,企业主流是 createBrowserRouter + RouterProvider;本课 Demo 因嵌在课程 Hub 里改用 MemoryRouter,API 一致。
  • 布局路由 + <Outlet> 对应 <router-view>;:id + useParams 取参(string)、path="*" 兜底 404。
  • <Link> / <NavLink> 是无刷新导航,useNavigate 对应 router.push/replace/back,useSearchParams 对应 route.query 且可直接写回。
  • 懒加载 = lazy(() => import()) + <Suspense>;守卫没有 beforeEach,用 <RequireAuth> 条件渲染 + <Navigate> 实现。

下一课(第 10 课 · 状态管理 Zustand):跨页面共享登录态、用户信息这类「不属于任何组件」的状态,Zustand 就是 Pinia 的 React 对应物——没有 actions 概念、一个 create 搞定,还附赠 Redux Toolkit 的「能看懂」指南。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值