嵌套路由与布局
路由介绍了最基础的用法:一个 Route 对应一个页面,各自有自己的 path 和 element。真实的应用很快就会遇到更复杂的情况,因为 URL 本身就是嵌套的。比如 /host/income 这样的路径有两个层级,往往深层路径对应的页面会保留浅层路径页面的大部分内容:相同的导航栏、相同的标签页,只是下面的内容换成了新的。嵌套路由就是 React Router 用来表达这种结构的方式。你把 Route 元素嵌套在彼此里面,路由器就会一次性渲染整个匹配链,让父元素包裹子元素。
所以"嵌套路由"涉及两个相关的概念。URL 的段嵌套了,UI 也随之嵌套:页面的某些部分在你深入导航时保持不变,只有内层部分会改变。第二点才是真正的收益。
用 Outlet 嵌套路由并渲染子元素
Route 既可以作为自闭合元素使用,也可以包含子元素。子元素必须是其他 Route 元素。当子路由匹配时,React Router 先渲染父路由的 element,父元素通过渲染 Outlet 来决定子元素在哪里显示。
import { BrowserRouter, Routes, Route } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="vans" element={<Vans />} />
</Route>
</Routes>
</BrowserRouter>
)
}import { Outlet } from 'react-router-dom'
import Header from './Header'
export default function Layout() {
return (
<>
<Header />
<Outlet />
</>
)
}Outlet 在父元素中挖出一个洞,让匹配的子元素在那里渲染。它的作用类似于组件组合中的 children prop:父元素渲染共享的结构,并标记出可变部分的位置。区别在于 React Router 来填充这个洞,根据当前 URL 选择匹配的子路由。如果没有 Outlet,父元素会占据整个页面,而被匹配的子元素则无处可渲染,这是调试嵌套页面空白的第一个检查点。
上面那个外层路由是一个布局路由:它有 element 但没有 path。无路径的路由会匹配其所有子路由匹配的 URL,所以 Layout 会包裹每一个页面,这正是放置网站级别的公共元素(如页头和页脚)的地方。把页头写在 Routes 旁边也能达到同样效果,但前提是应用只有一个布局;当像 /host 这样的部分需要用不同的公共元素而不改动下层页面时,布局路由就派上用场了。你应该避免的做法是在每个页面组件内部重复写页头。
带路径的布局
布局路由也可以带有 path,这样共享 UI 就只会限制在应用的某一部分。这个课程的 VanLife 项目有一个 /host 部分,带有自己的二级导航,所以宿主页面在第一个布局内又嵌套了第二个布局:
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="host" element={<HostLayout />}>
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
</Route>
</Route>HostLayout 渲染宿主导航以及一个 Outlet,所以访问 /host/income 时,路由器会依次渲染 Layout、然后在它的 Outlet 中渲染 HostLayout、再在 HostLayout 的 Outlet 中渲染 Income。三个路由同时匹配,每个父元素都框住下一个。布局可以嵌套得跟设计要求一样深。
索引路由
上面这个设置有一个缺陷:HostLayout 在 /host 处渲染,但它的 Outlet 那里没有内容可显示,因为每个子路由都会在路径上添加一个段。解决办法是索引路由:用 index prop 代替 path,这个子路由就会在父路由的 Outlet 中、父路由自己的路径处渲染。
<Route index element={<Dashboard />} />现在访问 /host 会显示宿主导航,Dashboard 在其中,而 /host/income 则会把 Dashboard 替换为收入页面。把 index 看作"默认子元素":在添加更深段之前,应该在 Outlet 中显示的内容。在顶层也适用,<Route index element={<Home />} /> 就是根布局内的主页,和开头例子中写的 path="/" 是同一个路由。
相对路径和相对链接
注意上面的子路由路径:income,没有开头的斜杠。以 / 开头的路径是绝对路径,从网站根目录开始计算。没有斜杠的路径相对于其父路由,所以嵌套的 income 意味着 /host/income。这样可以让深层路由树更容易维护:在一个地方把 host 改成 admin,所有后代路径都会跟着改。
链接也是一样。在路由元素内渲染的 Link 会根据渲染它的路由来解析相对路径,所以在 HostLayout 内的导航可以完全去掉 /host 前缀:
import { Link, Outlet } from 'react-router-dom'
export default function HostLayout() {
return (
<>
<nav>
<Link to=".">Dashboard</Link>
<Link to="income">Income</Link>
<Link to="reviews">Reviews</Link>
</nav>
<Outlet />
</>
)
}有两个特殊值借用了文件系统路径的含义。to="." 链接到当前路由,这样 Dashboard 链接就能指向 /host 本身,不需要硬编码。to=".." 向上爬升,对于"回到列表"这样的链接很有用;不过有个细微之处需要注意,它是爬升一个路由而非一个 URL 段,当一个路由拥有多个 URL 段时就开始重要,路由参数和位置会在真实的返回按钮中展示这个用法。
NavLink 和祖先匹配
把这些 Link 元素换成 NavLink 来给当前页面添加样式,课程在宿主导航中就是这样做的,祖先匹配就会出现:路由器会把当前 URL 的所有祖先都视为已匹配,所以 to="." 的 Dashboard 链接在 /host/income 和 /host/reviews 上也会显示为活跃状态。给那个链接加上 end 属性,它就只在 /host 处显示为活跃,在下面的任何路径上都不会。
用 useOutletContext 向下传递数据
布局常常拥有其子元素需要的数据。课程中的 HostVanDetail 获取一个车型数据,保存在 state 中,然后渲染一个标签导航和一个 Outlet,信息、定价或照片页面会在那里出现。Props 无法通过 Outlet 传递,因为布局从不直接渲染那些组件。React Router 的答案是在 Outlet 上使用 context prop,在另一端用 useOutletContext 读取:
<Outlet context={{ currentVan }} />import { useOutletContext } from 'react-router-dom'
export default function HostVanInfo() {
const { currentVan } = useOutletContext()
return <h4>Name: {currentVan.name}</h4>
}无论你传什么值给 context,useOutletContext 在当前填充 Outlet 的任何子元素中都会返回那个值。传一个对象,在接收端进行解构,这是常见的模式,这样以后还有空间添加更多值。如果你熟悉 Context,这会很熟悉:Outlet 充当一个作用域仅限于页面中一个洞的 provider。
嵌套还是不嵌套
嵌套在路由共享 UI 时才值得。这是判断的标准。光看 URL 可能会迷惑你:/vans 和 /vans/:id 共享一个段,为什么不共享一个路由呢?但在 VanLife 中,这两个页面除了网站级别的公共部分外没有任何共享的可见内容,嵌套它们会强制一个尴尬的结构:一个父路由有 path 但没有 element,只能回退到渲染一个裸露的 Outlet,加上一个 index 子路由来渲染列表。这样做结构更复杂,却没有任何收益。
当有共享 UI 需要保持在屏幕上时,嵌套路由并给父路由添加一个包含 Outlet 的布局元素。当唯一的动机就是避免重复一个路径段时,两个平级的路由是更简洁、更清晰的选择。
版本说明
这里的例子从 react-router-dom 导入,在 v6 和 v7 中都能用;路由介绍了 v7 在包上做的改动。嵌套路由的概念在两个版本中是一样的。
Outlet 处放置子页面。 没有路径但有 element 的路由是包裹所有内容的布局,index 路由是在父路由自己的地址处显示的默认子元素。
如果两个页面在屏幕上没有共享任何内容,就没有必要嵌套它们。
接下来:路由参数和位置,一个路由服务多个页面。

