第 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 数组 + createRouter | createBrowserRouter 数组 + RouterProvider | 同为配置式路由表,企业主流 |
<router-view /> | <Outlet /> | 嵌套子路由的渲染出口 |
component: () => import() | lazy(() => import()) + <Suspense> | React 要自己写加载占位 |
route.params | useParams() | 值都是 string,同样不会触发重新挂载 |
route.query | useSearchParams() | React 还给了直接写回的 setter |
router.push / replace / back | navigate(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 的「能看懂」指南。
本文系统讲解 React Router v6/v7 的核心用法,涵盖组件式与对象式路由配置、Layout+Outlet 布局嵌套、动态路由与 404 处理、Link/NavLink 编程式导航、React.lazy+Suspense 懒加载,以及基于守卫组件实现的登录拦截方案。强调企业级最佳实践,如路由表集中管理、查询参数状态持久化、权限守卫封装和常见避坑指南。
168

被折叠的 条评论
为什么被折叠?



