React Router 全面介绍 – wiki基地


React Router 全面介绍:构建强大的单页面应用路由

引言:为什么我们需要路由?

在传统的网页开发中,页面之间的跳转通常是通过点击 <a> 标签,触发浏览器向服务器发送新的请求,服务器返回新的 HTML 页面来实现的。这种模式叫做“服务器端路由”。

然而,随着前端技术的发展,单页面应用(Single Page Application,简称 SPA)变得越来越流行。SPA 的特点是整个应用只有一个 HTML 页面,所有内容的变化和页面的“跳转”都是通过 JavaScript 动态地操作 DOM 来实现的,用户在不同功能模块之间切换时,浏览器不会进行整页刷新。这带来了流畅的用户体验和更快的响应速度。

但在 SPA 中,如何管理不同的视图(或称之为“页面”),并让用户可以通过浏览器地址栏的 URL 来访问特定的视图,同时也能使用浏览器的前进/后退功能呢?这就需要“客户端路由”。

React Router 是 React 生态系统中事实上的标准路由库,它提供了一系列组件和钩子(Hooks),帮助我们在 React 应用中优雅地实现客户端路由,让用户感觉像是在浏览多个独立的页面,但实际上所有的切换都发生在同一个 HTML 页面内。

本文将深入探讨 React Router 的核心概念、主要组成部分以及如何使用它来构建功能丰富的单页面应用。我们将主要关注目前主流且推荐使用的 React Router v6 版本。

核心概念

理解 React Router 的核心概念是正确使用它的基础:

  1. 客户端路由 (Client-Side Routing):
    与服务器端路由不同,客户端路由由前端 JavaScript 代码控制。当用户点击一个链接时,JavaScript 拦截这个点击事件,阻止浏览器发送新的页面请求,而是通过修改浏览器地址栏的 URL (history API 或 URL hash) 并动态地渲染相应的组件,从而实现页面内容的更新。整个过程无刷新。

  2. 历史记录 (History):
    浏览器有一个内置的历史堆栈,用于记录用户访问过的页面,从而实现前进和后退功能。React Router 通过 history 库与浏览器历史记录 API 进行交互,管理 URL 的变化和历史堆栈的状态,确保客户端路由与浏览器的前进/后退功能同步。

  3. 声明式导航 (Declarative Navigation):
    React Router 倡导声明式编程。我们不是直接操作 DOM 或 history API 来改变路由,而是使用 React 组件来 声明 路由的匹配规则和导航链接。例如,使用 <Link> 组件来声明一个导航链接,使用 <Route> 组件来声明一个路径与组件的对应关系。

  4. 路由匹配 (Route Matching):
    React Router 根据当前的 URL 路径来匹配预先定义的路由规则 (<Route>),并渲染与匹配的路径相关联的组件。在 v6 中,Routes 组件会遍历其内部的 Route 子元素,一旦找到第一个匹配当前 URL 的 Route,就渲染该 `Route 指定的元素,并停止继续匹配。

React Router 的主要组成部分 (v6)

React Router 提供了不同的包,其中最常用的是 react-router-dom,它包含了用于 Web 应用的组件和钩子。以下是其核心组成部分:

1. Router 组件 (路由容器)

这是整个路由系统的入口,它负责创建并管理历史对象,并将其提供给应用中的其他路由组件。通常,你需要用其中一个 Router 组件包裹你的整个应用的最顶层组件。

  • <BrowserRouter>:
    这是最常用的 Router。它使用 HTML5 History API (pushState, replaceState, popstate 事件) 来管理 URL。它会创建干净的 URL,例如 /users/123。这需要服务器端进行一些配置,确保当用户直接访问这些 URL 时(例如输入地址或刷新页面),服务器能返回应用的入口 HTML 文件(通常是 index.html),而不是返回 404 错误。

  • <HashRouter>:
    它使用 URL 的哈希部分(#)来管理路由,例如 /index.html#/users/123。URL 的哈希部分不会发送到服务器,因此不需要服务器端进行额外的配置。但它的 URL 不如 BrowserRouter 简洁美观,且在处理一些高级功能时可能有限制。

用法示例:

“`jsx
// src/index.js 或 src/App.js (顶层文件)
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import { BrowserRouter } from ‘react-router-dom’; // 或 HashRouter
import App from ‘./App’;

const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(

{/ 用 BrowserRouter 包裹顶层 App 组件 /}




);
“`

2. Routes 组件 (路由集合容器)

在 React Router v6 中,<Routes> 组件取代了 v5 中的 <Switch>。它是用来包裹所有 <Route> 组件的容器。它的主要作用是查找与其子元素 <Route>path 属性匹配的第一个路由,并渲染其 element 属性指定的组件。

重要特性:
* 它只渲染第一个匹配的 <Route>,而不是所有匹配的。
* 它支持相对路径(relative paths)和嵌套路由(nested routes),极大地简化了路由配置。

用法示例:

“`jsx
// src/App.js
import React from ‘react’;
import { Routes, Route } from ‘react-router-dom’;
import HomePage from ‘./pages/HomePage’;
import AboutPage from ‘./pages/AboutPage’;
import ContactPage from ‘./pages/ContactPage’;
import NotFoundPage from ‘./pages/NotFoundPage’;
import Layout from ‘./components/Layout’; // 假设有一个布局组件

function App() {
return (

{/ 使用布局路由包裹需要共同布局的子路由 /}
}>
{/ Index Route:当路径完全匹配 “/” 时渲染 HomePage /}
} />
{/ 其他嵌套路由 /}
} />
} />
{/ 动态参数路由 /}
{/ } /> /}

  {/* 404 Fallback Route:匹配所有未匹配的路径 */}
  <Route path="*" element={<NotFoundPage />} />
</Routes>

);
}

export default App;
“`

3. Route 组件 (单个路由规则)

<Route> 组件是用来定义单个路由规则的。它最重要的两个属性是:

  • path: 指定需要匹配的 URL 路径。路径可以是静态的 (/about),也可以包含动态参数 (/users/:userId)。在 v6 中,path 也可以是相对路径,与父级 <Route>path 相结合。
  • element: 指定当 path 匹配时需要渲染的 React 元素(通常是一个组件)。在 v6 中,使用 element 属性替代了 v5 中的 componentrender 属性,并且需要传入一个 JSX 元素(如 <HomePage />),而不是组件类或函数本身。

动态参数 (:paramName):
可以在 path 中使用冒号 : 来定义动态参数。例如,/users/:userId 可以匹配 /users/123/users/abc:userId 部分将作为参数被捕获,并通过 useParams 钩子获取。

4. 导航组件

这些组件用于在应用内部创建导航链接。它们避免了使用传统的 <a> 标签(因为 <a> 标签会导致整页刷新)。

  • <Link>:
    这是最基本的导航组件。它的 to 属性指定要跳转的目标路径。点击 <Link> 会改变 URL 并触发路由匹配,但不刷新页面。

    “`jsx
    import { Link } from ‘react-router-dom’;

    // …
    关于我们
    用户 123
    {/ 带有 state 参数的链接 /}
    控制面板
    ``to属性也可以是一个对象{ pathname: ‘/path’, search: ‘?query=string’, hash: ‘#hash’, state: { / state / } }state属性可以在目标路由组件中通过useLocation` 钩子获取,用于传递一些非 URL 参数的状态。

  • <NavLink>:
    <Link> 类似,但 <NavLink> 提供了额外的功能,可以根据链接是否“活跃”(即当前 URL 是否与链接的 to 路径匹配)来应用特殊的样式或类名。这非常适合用在导航菜单中,高亮显示当前用户所在的页面链接。

    NavLink 使用函数作为 classNamestyle 属性的值时,会接收一个包含 isActive 布尔值的对象作为参数。

    “`jsx
    import { NavLink } from ‘react-router-dom’;

    // …
    isActive ? ‘active-link’ : ”}
    style={({ isActive }) => ({ color: isActive ? ‘red’ : ” })}

    关于我们

    “`

5. 导航钩子 (Hooks)

React Router v6 提供了一系列钩子,让你可以在函数组件中访问路由状态或进行编程式导航。

  • useNavigate():
    返回一个 navigate 函数,用于进行编程式导航(例如,在表单提交后跳转、点击按钮跳转)。

    “`jsx
    import { useNavigate } from ‘react-router-dom’;

    function MyComponent() {
    const navigate = useNavigate();

    const handleClick = () => {
    // 跳转到指定路径
    navigate(‘/dashboard’);
    // 携带 state 跳转
    // navigate(‘/login’, { state: { from: ‘/dashboard’ } });
    // 后退一步
    // navigate(-1);
    // 前进一步
    // navigate(1);
    };

    return (

    );
    }
    ``navigate函数可以接受一个路径字符串或一个数字(负数表示后退,正数表示前进),也可以接受一个路径对象{ pathname, search, hash, state }`。

  • useLocation():
    返回当前的 location 对象,该对象包含了当前 URL 的详细信息,如 pathname(路径)、search(查询字符串)、hash(URL 哈希)和 state(通过 Linknavigate 传递的状态)。

    “`jsx
    import { useLocation } from ‘react-router-dom’;

    function MyComponent() {
    const location = useLocation();
    console.log(location.pathname); // ‘/current/path’
    console.log(location.search); // ‘?query=string’
    console.log(location.state); // { from: ‘homepage’ } (如果存在)

    return (

    当前路径: {location.pathname}

    );
    }
    “`

  • useParams():
    返回一个对象,包含当前路由中动态参数的键值对。适用于路径中定义了 :paramName 的情况。

    “`jsx
    import { useParams } from ‘react-router-dom’;

    // Route path: “/users/:userId/:tab”
    function UserProfile() {
    const params = useParams();
    console.log(params.userId); // 从 URL 中获取的 userId
    console.log(params.tab); // 从 URL 中获取的 tab

    return (

    用户 ID: {params.userId}, 当前 Tab: {params.tab}

    );
    }
    “`

  • useSearchParams():
    返回一个数组,第一个元素是 URLSearchParams 对象,用于读取和操作 URL 的查询字符串(?key1=value1&key2=value2);第二个元素是一个函数,用于更新查询字符串。

    “`jsx
    import { useSearchParams } from ‘react-router-dom’;

    // URL: “/products?category=electronics&sort=price”
    function ProductsPage() {
    const [searchParams, setSearchParams] = useSearchParams();

    const category = searchParams.get(‘category’); // ‘electronics’
    const sort = searchParams.get(‘sort’); // ‘price’

    const handleCategoryChange = (newCategory) => {
    // 更新查询参数,并触发页面重新渲染
    setSearchParams({ category: newCategory, sort: sort });
    };

    return (

    产品列表

    当前分类: {category}, 排序方式: {sort}

    );
    }
    ``setSearchParams函数可以接受一个对象或一个新的URLSearchParams` 实例来更新查询字符串。

  • useOutlet() / <Outlet>:
    useOutlet() 钩子和 <Outlet> 组件用于实现嵌套路由。<Outlet> 是一个占位符,用于在其父级路由元素中渲染匹配到的子级路由元素。useOutlet() 钩子可以获取由 <Outlet> 渲染的子元素的引用。

    “`jsx
    // components/Layout.js (父级路由元素中使用的布局组件)
    import { Outlet } from ‘react-router-dom’;
    import Navigation from ‘./Navigation’; // 导航组件

    function Layout() {
    return (

    {/ 共享的导航 /}

    {/ 子路由匹配到的元素将在这里渲染 /}

    {/ 共享的页脚 /}
    © 2023 我的应用

    );
    }
    export default Layout;

    // App.js (路由配置)
    import { Routes, Route } from ‘react-router-dom’;
    import Layout from ‘./components/Layout’;
    import HomePage from ‘./pages/HomePage’;
    import AboutPage from ‘./pages/AboutPage’;
    // … 其他页面组件

    function App() {
    return (

    {/ Layout 是一个父级 Route,它的 element 渲染 Layout 组件 /}
    {/ Layout 组件内部有 /}
    }>
    {/ 这些是 Layout 的子路由,它们将在 Layout 的 位置渲染 /}
    } /> {/ 当路径是 “/” 时渲染 HomePage /}
    } />
    {/ 嵌套更深层的路由 /}
    {/ } /> /}

    {/ 其他顶级路由 /}
    {/ } /> /}

    );
    }
    ``
    这种嵌套路由的模式是 v6 的一大改进,它让布局和路由结构更加清晰,并且使得构建复杂的应用界面变得更加容易。
    index` 属性用于指定当父路径完全匹配时渲染的默认子路由。

  • <Navigate>:
    一个组件,用于在渲染时进行导航(重定向)。它取代了 v5 中的 useHistory().replace<Redirect>

    “`jsx
    import { Navigate } from ‘react-router-dom’;

    function OldRouteComponent() {
    // 当这个组件渲染时,立即重定向到新路径
    return ;
    // replace={true} 表示替换当前历史记录,而不是将其添加到历史堆栈
    }

    // 结合条件渲染实现保护路由(示例)
    function ProtectedRoute({ element, isAuthenticated }) {
    if (isAuthenticated) {
    return element;
    } else {
    // 如果未认证,重定向到登录页,并带上从哪个页面来的 state
    return ;
    }
    }

    // 在 Routes 中使用
    // } isAuthenticated={user.isAuthenticated} />} />
    ``` 组件非常适合用于需要根据某些条件进行页面跳转的场景,例如登录验证、权限检查或处理旧的 URL。

路由配置与匹配详解 (v6)

  • <Routes><Route> 的关系:
    <Routes> 的作用是查找并渲染第一个匹配其子级 <Route> 的元素。在 v6 中,匹配算法经过优化,更好地支持嵌套路由。
    例如:
    jsx
    <Routes>
    <Route path="/users" element={<UsersLayout />}>
    <Route index element={<UsersList />} /> // 匹配 /users
    <Route path=":userId" element={<UserProfile />} /> // 匹配 /users/:userId
    <Route path="add" element={<AddUser />} /> // 匹配 /users/add
    </Route>
    <Route path="/about" element={<About />} /> // 匹配 /about
    <Route path="*" element={<NotFound />} /> // 匹配其他所有路径
    </Routes>

    当 URL 是 /users/123 时,<Routes> 会首先匹配到 /users 这个父级 Route。然后它会查看这个父级 Routeelement (<UsersLayout />) 内部的 <Outlet>。接着,它会继续在 /users 的子路由中查找与 /users/123 相对于 /users 的剩余路径 (123) 匹配的路由。它会匹配到 :userId 这个子 Route (因为 123 匹配 :userId),并在 <Outlet> 的位置渲染 <UserProfile />

  • 相对路径 (Relative Paths):
    在 v6 中,path 属性默认是相对于其父级 <Route>path 的。例如,如果父级 Routepath/users,那么子级 Routepathadd 将匹配 /users/add。如果想要定义一个绝对路径,可以在路径前加上 /,例如 <Route path="/dashboard" ...>Linknavigate 也可以使用相对路径。

  • 通配符 (*):
    path="*" 是一个非常有用的匹配规则,它可以匹配任何路径。通常将其放在 <Routes> 的最后一个,用作 404 页面或备用页面的处理。例如 <Route path="*" element={<NotFoundPage />} />

  • Index Route (index 属性):
    当一个父级 Routepath 被精确匹配时,例如 URL 是 /users,并且你希望渲染一个默认的子组件(比如用户列表),而不是父级布局组件中的 <Outlet> 为空,可以使用 index 属性。它表示当父级路径精确匹配时,渲染这个带有 index 属性的 Routeelement。这取代了 v5 中通过重复父路径来实现默认子路由的方式。

    jsx
    <Route path="/users" element={<UsersLayout />}>
    <Route index element={<UsersList />} /> {/* URL 是 /users 时,在 UsersLayout 的 <Outlet> 中渲染 UsersList */}
    <Route path=":userId" element={<UserProfile />} /> {/* URL 是 /users/:userId 时,在 UsersLayout 的 <Outlet> 中渲染 UserProfile */}
    </Route>

安装 React Router

使用 npm 或 yarn 安装 react-router-dom 包:

“`bash
npm install react-router-dom

或者

yarn add react-router-dom
“`

示例代码结构

一个典型的 React Router 项目结构可能如下所示:

my-app/
├── public/
│ └── index.html
├── src/
│ ├── index.js # 应用入口,包裹 <BrowserRouter>
│ ├── App.js # 根组件,包含 <Routes> 配置
│ ├── pages/ # 页面组件
│ │ ├── HomePage.js
│ │ ├── AboutPage.js
│ │ ├── ContactPage.js
│ │ └── NotFoundPage.js
│ ├── components/ # 可复用组件
│ │ ├── Layout.js # 包含 <Outlet> 的布局组件
│ │ └── Navigation.js # 包含 <NavLink> 的导航组件
│ └── index.css # 样式
└── package.json

src/index.js:
“`jsx
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import { BrowserRouter } from ‘react-router-dom’;
import App from ‘./App’;
import ‘./index.css’; // 引入样式

const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(

{/ 包裹整个应用 /}



);
“`

src/App.js:
“`jsx
import React from ‘react’;
import { Routes, Route } from ‘react-router-dom’;
import Layout from ‘./components/Layout’;
import HomePage from ‘./pages/HomePage’;
import AboutPage from ‘./pages/AboutPage’;
import ContactPage from ‘./pages/ContactPage’;
import NotFoundPage from ‘./pages/NotFoundPage’;
import UserProfile from ‘./pages/UserProfile’; // 假设新增用户详情页

function App() {
return (

{/ 顶层布局路由 /}
}>
{/ Index Route /}
} />
{/ 普通页面路由 /}
} />
} />
{/ 动态参数路由 /}
} />
{/ 如果 /users 路径需要一个默认视图(如用户列表),可以再加一个 Index Route 或普通 Route /}
{/ } /> /}

  </Route>

  {/* 404 页面 */}
  <Route path="*" element={<NotFoundPage />} />
</Routes>

);
}

export default App;
“`

src/components/Layout.js:
“`jsx
import React from ‘react’;
import { Outlet } from ‘react-router-dom’;
import Navigation from ‘./Navigation’; // 导航组件

function Layout() {
return (

我的应用

{/ 导航菜单 /}


{/ 子路由内容将渲染在这里 /}

底部信息 © 2023

);
}

export default Layout;
“`

src/components/Navigation.js:
“`jsx
import React from ‘react’;
import { NavLink } from ‘react-router-dom’;
import ‘./Navigation.css’; // 引入样式

function Navigation() {
return (

);
}

export default Navigation;
**`src/pages/UserProfile.js`:**jsx
import React from ‘react’;
import { useParams, useLocation } from ‘react-router-dom’;

function UserProfile() {
const { userId } = useParams(); // 获取动态参数 userId
const location = useLocation(); // 获取 location 对象,可能包含 state

// 示例:从 state 中获取数据
const fromPage = location.state?.from || ‘未知来源’;

return (

用户详情

用户 ID: {userId}

访问来源: {fromPage}

{/ 根据 userId 加载和显示用户详细信息 /}

);
}

export default UserProfile;
“`

React Router v5 与 v6 的主要区别

从 v5 迁移到 v6 的开发者需要注意以下主要变化:

  1. Switch -> Routes: <Switch> 组件被 <Routes> 取代。<Routes> 的匹配算法更简洁且支持嵌套路由的相对路径匹配。
  2. 路由定义方式: v5 中 <Route> 使用 componentrender 属性来渲染组件,v6 统一使用 element 属性,并且需要传入一个 JSX 元素 <Component />
    • v5: <Route path="/about" component={AboutPage} /><Route path="/about" render={() => <AboutPage />} />
    • v6: <Route path="/about" element={<AboutPage />} />
  3. 嵌套路由: v6 极大地改进了嵌套路由的实现方式。通过在父级 <Route> 中嵌套子级 <Route>,并在父级组件中使用 <Outlet> 组件作为子路由的占位符。相对路径是默认行为。
  4. useHistory -> useNavigate: useHistory Hook 被 useNavigate Hook 取代。useNavigate 返回一个函数,用于进行编程式导航,而不是像 history 对象那样直接调用 pushreplace 方法。
  5. useRouteMatch -> useMatch: useRouteMatch Hook 被更简单的 useMatch Hook 取代,用于获取当前路由的匹配信息。
  6. exact 属性: 在 v6 的 <Routes> 中, <Route> 默认就是精确匹配的(或者说匹配行为更符合直觉,不需要 exact 来区分父子路由)。在嵌套路由中,父级 path 会匹配 URL 的一部分,子级 path 匹配剩余部分。顶层的 Index Route 使用 index 属性来实现父路径的精确匹配。
  7. 重定向: v5 使用 <Redirect> 组件,v6 使用 <Navigate> 组件。

这些改变使得 v6 的路由配置更加直观,特别是对于嵌套布局和路由的处理。

总结

React Router 是构建现代 React 单页面应用不可或缺的工具。它提供了一套强大、灵活且遵循 React 声明式编程范式的路由解决方案。通过 BrowserRouterHashRouter 管理历史记录,RoutesRoute 定义路由规则,LinkNavLink 实现声明式导航,以及 useNavigate, useLocation, useParams, useSearchParams 等 Hooks 赋予函数组件处理路由的能力。特别是 v6 版本引入的嵌套路由和 Hooks,极大地提升了开发效率和代码的可维护性。

掌握 React Router 的使用,能够帮助你更好地组织应用的 UI 结构,提供优秀的用户体验,并构建出功能丰富、易于维护的单页面应用。希望本文对你全面理解和使用 React Router 有所帮助。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部