前置:路由
目标:用嵌套
Route+Outlet做布局(顶栏/侧栏只写一次),并能独立设计多层路由树。
引言
后台常见结构:顶栏 + 侧栏固定,中间内容随 URL 变。
若每个页面都复制一遍壳子,改导航要改 N 处,登录态、面包屑、权限也无处安放。
嵌套路由 解决的正是这件事:父路由渲染布局,子路由填进布局里的 <Outlet />(挖好的洞)。
本篇 demo:Shell 包住「概览 / 设置」;设置里再嵌一层「资料」。
动手
完整代码:react2/13-nested-routes/demo
bash
cd react2/13-nested-routes/demo
npm install && npm run dev
点「概览 / 设置 / 资料」,看顶栏一直在,只有内容区换。
1. 先把「URL 树」和「组件树」对上
嵌套路由不是新 API,它只是把两棵树对齐:
text
URL 组件树
/ Shell → Home
/settings Shell → Settings
/settings/profile Shell → Settings → Profile
记住一句话:URL 有几层,组件树就可以有几层;<Outlet /> 是每一层的插槽。
对照 demo 的实际渲染结果:
| 访问地址 | 同时挂载的组件 | 内容区显示 |
|---|---|---|
/ |
Shell → Home |
概览 |
/settings |
Shell → Settings |
设置布局(Outlet 为空) |
/settings/profile |
Shell → Settings → Profile |
资料 |
Shell 和 Settings 都是「布局组件」,它们本身不直接对应某个业务结果,只负责壳和插槽。
2. 父布局 + Outlet
tsx
function Shell() {
return (
<main>
<nav>
<Link to="/">概览</Link>
<Link to="/settings">设置</Link>
<Link to="/settings/profile">资料</Link>
</nav>
<Outlet />
</main>
)
}
Shell 自己不对应最终「业务页」,只提供壳;<Outlet /> 位置渲染匹配到的子路由。
几个容易忽略的点:
Outlet不是「可选装饰」,没有它子路由就无家可归,页面会只剩布局。- 一个布局里可以放多个
Outlet,但通常一个就够;多出口属于进阶玩法。 - 布局组件里可以正常用 hooks(
useLocation、useNavigate),它仍然是普通组件。
3. 路由表怎么嵌
tsx
<Routes>
<Route element={<Shell />}>
<Route index element={<Home />} />
<Route path="settings" element={<Settings />}>
<Route path="profile" element={<Profile />} />
</Route>
</Route>
</Routes>
| 写法 | 含义 |
|---|---|
无 path 的父 Route |
只提供 layout(本 demo 的 Shell) |
index |
父路径本身,如 / → Home |
path="settings" |
相对父路径 → /settings |
再嵌 profile |
→ /settings/profile;Settings 里也要有 <Outlet /> |
tsx
function Settings() {
return (
<section>
<h2>设置布局</h2>
<Link to="/settings/profile">去资料子页</Link>
<Outlet />
</section>
)
}
访问 /settings/profile 时:Shell → Settings → Profile 三层同时挂着。
小提醒:
index只能出现在「有父级 path 或布局」的位置;它表示「父路径的默认子页」,不是单独的 URL 段。
4. 相对路径 vs 绝对路径
子路由的 path 默认是相对父路径的,这是嵌套路由能层层拼接的关键。
| 子路由写法 | 实际匹配 |
|---|---|
<Route path="settings"> 挂在 Shell 下 |
/settings |
<Route path="profile"> 挂在 settings 下 |
/settings/profile |
<Route path="/settings/profile"> 挂在 Shell 下 |
绝对路径,直接匹配 /settings/profile |
新手建议:
- 布局嵌套用相对路径,层级关系一眼能看懂。
- 只有确实想「跳出父级、从根开始」时才用绝对路径。
- 写绝对路径却挂在错误的父级下,容易出现「URL 对、页面结构不对」的诡异现象。
5. 导航高亮与 404
真实后台还会关心两件事:当前菜单高亮、未知地址兜底。
高亮用 NavLink,它会根据匹配状态给 className:
tsx
import { NavLink } from 'react-router-dom'
<NavLink
to="/settings"
className={({ isActive }) => (isActive ? 'active' : undefined)}
>
设置
</NavLink>
404 兜底通常放在最外层,且不要塞进业务布局里(否则 404 页面也会带后台侧栏):
tsx
<Routes>
<Route element={<Shell />}>
<Route index element={<Home />} />
<Route path="settings" element={<Settings />}>
<Route path="profile" element={<Profile />} />
</Route>
</Route>
<Route path="*" element={<p>页面不存在</p>} />
</Routes>
6. 布局给子路由传数据(进阶)
有时布局里有筛选条件、主题、当前用户,子页需要读。可以用 useOutletContext:
tsx
import { useState } from 'react'
import { Outlet, useOutletContext } from 'react-router-dom'
function Shell() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<>
<button onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>
切换主题
</button>
<Outlet context={{ theme }} />
</>
)
}
function Profile() {
const { theme } = useOutletContext<{ theme: string }>()
return <p>当前主题:{theme}</p>
}
它不是全局状态,只沿「布局 → 子路由」这条线传值;跨业务的共享仍应交给 Context / Zustand。
7. 对照表
| 概念 | 干什么 | demo |
|---|---|---|
| 布局路由 | 公共壳只写一次 | Shell |
Outlet |
子路由出口 | Shell / Settings 各一处 |
index |
父 URL 默认页 | / → Home |
相对 path |
拼出层级 URL | settings / profile |
通配 * |
404 兜底 | 可加在最外层 |
NavLink |
当前导航高亮 | 可替换 Link |
useOutletContext |
布局向子路由传值 | 进阶示例 |
8. 常见坑
| 做法 | 问题 |
|---|---|
布局里忘了 <Outlet /> |
子路由匹配了但空白 |
子路由写成绝对 /settings/profile 却挂错层级 |
嵌套关系乱;新手先用相对 path |
| 每个页面再包一遍同一导航 | 失去嵌套布局意义 |
index 和 path="" 混用 |
语义不清,容易出现重复匹配 |
| 把 404 放进后台布局 | 404 页也显示侧栏,体验奇怪 |
| 和「鉴权守卫」混淆 | 守卫也是布局路由套路,见 19 篇 |
9. 调试清单
页面空白或层级不对时,按顺序排查:
- 浏览器地址栏 URL 是否与路由表预期一致?
- 每一层布局组件里是否都有
<Outlet />? - 子路由
path是相对还是绝对?是否拼错层级? Routes外层是否有BrowserRouter?- 打开 React DevTools,看组件树里
Shell / Settings / Profile是否按预期嵌套。
demo 在练什么
- 一层布局
Shell+Outlet - 设置下再嵌
profile - 用
Link在层级间跳转,观察 URL 与 UI
自己改一改:
- 在
Shell旁加/about; - 给 settings 加
index默认页; - 把
Link换成NavLink,给当前菜单加高亮样式; - 在最外层加
path="*"的 404 页。