Skip to content
This page has been auto-translated and may contain errors.View in English

路由

React 应用将一个组件树渲染到一个 HTML 文档里。真实的网站有首页、关于页面、详情页面,每个都有自己的 URL。路由就是让这两件事配合的机制:URL 决定了渲染哪些组件,在应用中点击会更新 URL。React Router 是大多数 React 应用都在用的库,这个课程用整个章节来讲它,期间会构建一个名叫 VanLife 的车辆租赁应用。本章涵盖核心概念;嵌套路由路由参数在此基础上扩展。

客户端路由:MPA vs SPA

在传统的多页面应用(MPA)中,每次导航都是一次往返。浏览器向服务器请求 /about,服务器组装一个 HTML 页面并发送回来,浏览器丢弃当前页面加载新的。整页切换在每次点击时都会产生明显的闪烁。

单页面应用(SPA)只加载一次文档。这个名字有点误导:从用户的角度看,网站仍然可以有很多页面。单一的是文档本身。第一次加载后,React 应用本身决定了每个 URL 显示什么。导航到 /about 时,组件在运行的应用内部切换,无需请求新文档,也没有闪烁。如果某个视图需要新的数据,应用会在后台获取 JSON 并就地更新,这是获取数据章节讲的模式。

**客户端路由**是让这一切工作的机制:一个库监听 URL、拦截导航,并渲染匹配的组件,而不是让浏览器重新加载。保持文档活跃带来真正的好处。整页加载会清除所有 React 状态;留在一个文档内意味着你的状态在每次导航后都能存活。

版本说明

本课程讲授 React Router 6。版本 7 将 React Router 与 Remix 合并并整合了包,所以 react-router 是当前的导入路径。在 v7 中,react-router-dom 作为重新导出保留,是 v6 和本课程使用的包,这就是为什么本手册中的每个示例都从它导入;版本 8 删除了它,所以新安装时从 react-router 导入这些组件。

React Router 文档记录了三种模式:声明式(本章讲的 BrowserRouterRoute 组件);数据模式(将 createBrowserRouter 与 loaders 和 actions 配对);以及框架模式(一个 Vite 插件,添加了类型化路由模块、代码分割和服务器渲染)。本章中的组件和 hook 名称在这些版本间保持不变。

BrowserRouter、Routes 和 Route

三个组件定义了基本设置:BrowserRouter 包裹应用并启用路由,Routes 保存你的路由定义,每个 Route 将一个路径映射到一个元素。

jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  )
}

BrowserRouter 在幕后是一个上下文提供者。用它包裹应用,让下面的每个组件都能访问路由的工具,这就是为什么它要放在最上层。有些代码库在导入时重命名它(import { BrowserRouter as Router }),因为全名太长。

每个 Route 接受两个 props。path 是 URL 中域名后面的部分:首页是 /,关于页面是 /aboutelement 是 URL 匹配该路径时要渲染的 JSX,作为一个实际的元素写,如 <Home /> 而不是 Home。当 URL 是 /about 时,React Router 在 Routes 组件所在的位置渲染 <About />Routes 外面的任何内容都在每个页面上渲染。

访问没有匹配路由的路径时,什么都不会渲染。下面的通配符路由修复了这个问题。

在 HTML 中,在页面间移动的方式是锚标签,但对 SPA 来说完全是错的:点击 <a href="/about"> 会触发整页加载,丢弃运行的应用和其中的每一部分状态。React Router 的答案是 Link 组件。

jsx
import { Link } from 'react-router-dom'

function Header() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
    </nav>
  )
}

Link 接受 to prop 而不是 href,读起来很自然:链接到下面的路由。在浏览器中它仍然渲染为真实的锚标签,所以屏幕阅读器、右键菜单和你的 CSS 选择器都会看到普通的 <a>。区别在于点击处理器:React Router 拦截点击,更新 URL,并交换渲染的组件,全部无需重新加载文档。一个显示为十的计数器在你浏览页面间移动时保持为十。在 React Router 应用内,内部导航总是通过 Link 进行;普通锚标签用于外部 URL。

导航栏通常会高亮显示你当前所在的页面。NavLink 就是为此而生的:它的行为像 Link,除了它的 classNamestyle props 可以接受函数而不是普通值。React Router 用一个包含 isActive 布尔值的对象调用该函数,当链接的路由与当前 URL 匹配时为真。这是渲染 props 模式应用到 children 之外的 prop。

jsx
import { NavLink } from 'react-router-dom'

function Header() {
  return (
    <nav>
      <NavLink
        to="/about"
        className={({ isActive }) => isActive ? 'active-link' : ''}
      >
        About
      </NavLink>
    </nav>
  )
}

函数返回什么就成为类名,所以 active-link 类只在 /about 是当前路由时应用,你的 CSS 处理其余的。style prop 的工作方式相同,使用内联样式对象:

jsx
const activeStyles = { fontWeight: 'bold', textDecoration: 'underline' }

<NavLink
  to="/about"
  style={({ isActive }) => isActive ? activeStyles : null}
>
  About
</NavLink>

类名还是内联样式是团队的约定;两者都完全支持,所以跟随项目已有的做法。

通配符 404 路由

用户可能会访问不存在的路径:过时的链接、打字错误、别人的损坏链接。没有备用方案,React Router 在那里什么都不渲染。解决办法是一个通配符路由,有时叫 splat 路由,其路径是单个星号。

jsx
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} />
</Routes>

path="*" 匹配任何其他路由不声称的内容,很像 CSS 中的通用选择器。典型的 NotFound 组件配对一条简短消息和一个回到首页的 Link。列表中的顺序无关紧要:React Router 6 对每个路由和 URL 评分并选择最佳匹配,所以通配符只在没有更具体的匹配时获胜。把它放在最后是约定和可读性而不是必要性。

这一切背后的机制是浏览器的 History API。Link 在普通左键点击上调用 event.preventDefault(),用 history.pushState 推送新 URL,让路由的上下文提供者通知订阅者位置已更改。匹配的路由重新渲染;文档从不重新加载。这也是状态为什么能存活的全部原因:JavaScript 环境从未被摧毁。

v6 中的路由匹配是基于排名的。每个路径段赚取分数,静态段胜过动态段,动态段胜过通配符,所以定义顺序没有意义。这取代了 v5 的先匹配先赢模式,列表顶部的无限制 / 会遮蔽下面的所有内容。

NavLink 将祖先视为活跃

路由器将当前 URL 的每个祖先视为匹配,所以指向 /host 的链接在你位于 /host/income 时报告 isActive。通常这就是你在导航栏中想要的。当不是时,end prop 告诉该链接仅匹配其确切路径。这在嵌套路由中最重要,多个路由按设计同时渲染。

客户端路由也有一个部署陷阱。BrowserRouter 产生干净的 URL 如 /about,但服务器从未听说过那个路径;只有路由知道。用户刷新或深度链接到那里时直接点击服务器,所以主机必须配置为为每个路由提供 index.html 并让 React Router 从 URL 接管。大多数静态主机都有一条就为此而生的单行重写规则。

服务器设置也是 v7 框架模式改变局面的地方。声明式和数据模式将构建和服务器留给你,而框架模式用 Vite 插件接管两者,处理服务器渲染。本章中的所有内容都在那条线的声明式一侧。

JunoURL 选择组件 单页面应用加载一个文档,然后当你点击时交换你看到的内容,所以没有闪烁,什么都不会被丢弃。

BrowserRouter 包裹你的应用,在 Routes 内列出你的页面作为 Route 组件,每个 path 获得自己的 element 来渲染。

使用 Link 而不是锚标签,这样点击会保持应用运行,添加一个 path="*" 路由,这样误输 URL 的访问者会看到友好的页面而不是空白屏幕。

JunoURL 选择组件 工作集很小:顶部的 BrowserRouter,持有 Route 元素的 Routes,这些元素将 path 映射到 elementLink to 用于导航,当导航栏应该高亮当前页面时用 NavLink

传递 classNamestyle 一个函数,从它接收的对象读取 isActive

path="*" 通配符完成设置以获得 404 页面;路由评分意味着它在列表中的位置无关。

JunoURL 选择组件Link 阻止默认点击并驱动 history.pushStateBrowserRouter 是上下文提供者,广播位置变化,这就是为什么状态在导航后存活。

匹配是基于排名而不是顺序,祖先对 NavLink 计为活跃直到你添加 end,干净的 URL 需要服务器将每个路径重写到应用外壳。

声明式模式的 API 直接沿用到 v7,它也记录了用于 loaders 和 actions 的数据模式以及用于全栈集成的框架模式。

接下来:嵌套路由和布局,路由获得共享的通用部分和路由中的路由。