React全家桶笔记(六):React Router 5 全解

3 阅读9分钟

React全家桶笔记(六):React Router 5 全解

本篇全面讲解 React Router 5 的核心概念与使用方式,从 SPA 理解到路由原理,再到嵌套路由、路由参数、编程式导航等实战技巧。 📺 对应张天禹react全家桶视频:P74 - P93


一、SPA 应用的理解(P74)

1.1 什么是 SPA?

SPA(Single Page Application)— 单页面应用:

  • 整个应用只有一个完整的页面(index.html)
  • 点击页面中的链接不会刷新页面,只会做页面的局部更新
  • 数据都需要通过 Ajax 请求获取,并在前端异步展现

1.2 SPA vs MPA

SPA(单页应用):
├── 一个 HTML 页面
├── 前端路由控制页面切换
├── 局部刷新,体验流畅
├── 首屏加载较慢(需要加载整个应用)
└── SEO 不友好(可通过 SSR 解决)

MPA(多页应用):
├── 多个 HTML 页面
├── 每次跳转都是整页刷新
├── 服务端路由
├── 首屏加载快(只加载当前页)
└── SEO 友好

二、路由的理解(P75)

2.1 什么是路由?

一个路由就是一个映射关系:key → value

  • key 是路径(path)
  • value 根据路由类型不同而不同

2.2 路由分类

后端路由:
├── key:URL 路径
├── value:服务器端的处理函数(function)
├── 工作方式:服务器收到请求,根据路径匹配对应的函数处理并返回响应
└── 例:Node.js 的 app.get('/api/users', handler)

前端路由:
├── key:URL 路径
├── value:React 组件(component)
├── 工作方式:浏览器路径变化时,匹配对应的组件进行渲染
└── 例:<Route path="/home" component={Home}/>

三、前端路由原理(P76)

前端路由的核心依赖浏览器的 History API

3.1 History 模式

// 浏览器的 history 对象
// 方法1:直接使用 H5 的 history API
history.pushState(state, title, url)   // 压入一条历史记录
history.replaceState(state, title, url) // 替换当前历史记录
history.back()     // 后退
history.forward()  // 前进
history.go(n)      // 前进/后退 n 步

// 方法2:使用 hash(锚点)
// URL 中 # 后面的内容变化不会引起页面刷新,但会被记录到历史记录中
// 例:localhost:3000/#/home

3.2 BrowserRouter vs HashRouter(预告)

BrowserRouter:
├── 使用 H5 的 history API
├── URL 格式:localhost:3000/home
└── 需要服务器端配合(刷新时返回 index.html)

HashRouter:
├── 使用 URL 的 hash 值
├── URL 格式:localhost:3000/#/home
└── 不需要服务器端配合(hash 值不会发送给服务器)

四、路由的基本使用(P77)

4.1 安装

npm install react-router-dom@5

4.2 基本结构

// index.js — 用 BrowserRouter 包裹整个 App
import { BrowserRouter } from 'react-router-dom'

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
)
// App.jsx
import { Link, Route } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'

export default class App extends Component {
  render() {
    return (
      <div>
        <div className="nav">
          {/* 编写路由链接(导航区) */}
          <Link className="list-group-item" to="/about">About</Link>
          <Link className="list-group-item" to="/home">Home</Link>
        </div>
        <div className="content">
          {/* 注册路由(展示区) */}
          <Route path="/about" component={About} />
          <Route path="/home" component={Home} />
        </div>
      </div>
    )
  }
}

核心步骤

  1. <BrowserRouter><HashRouter> 包裹整个应用
  2. <Link to="/xxx"> 编写路由链接(替代 <a> 标签)
  3. <Route path="/xxx" component={Xxx}/> 注册路由

⚠️ 易错点<Link><Route> 必须被同一个 Router 包裹。最佳实践是在 index.js 中用 Router 包裹 <App/>,这样整个应用都在同一个 Router 上下文中。


五、路由组件与一般组件(P78)

5.1 区别

一般组件:
├── 写法:<Header/>
├── 存放位置:src/components/
└── 接收的 props:父组件传什么就收到什么

路由组件:
├── 写法:<Route path="/home" component={Home}/>
├── 存放位置:src/pages/(或 src/views/)
└── 接收的 props:路由器自动传递三个重要对象 ⬇️

5.2 路由组件收到的 props

// 路由组件的 props 中自动包含三个重要对象:
this.props = {
  history: {
    go: f,
    goBack: f,
    goForward: f,
    push: f,
    replace: f,
  },
  location: {
    pathname: "/about",
    search: "",
    state: undefined,
  },
  match: {
    params: {},
    path: "/about",
    url: "/about",
  }
}

💡 文件组织建议:路由组件放 pages/ 目录,一般组件放 components/ 目录,这样项目结构更清晰。


六、NavLink 与封装(P79-P80)

6.1 NavLink 的使用(P79)

NavLink 可以在路由链接被选中时自动添加一个 active 类名(高亮效果):

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

{/* activeClassName 指定选中时的类名,默认是 "active" */}
<NavLink activeClassName="selected" className="list-group-item" to="/about">About</NavLink>
<NavLink activeClassName="selected" className="list-group-item" to="/home">Home</NavLink>

6.2 封装 NavLink 组件(P80)

当多个 NavLink 有大量重复属性时,可以封装一个通用组件:

// components/MyNavLink/index.jsx
import { NavLink } from 'react-router-dom'

export default class MyNavLink extends Component {
  render() {
    // this.props.children 就是标签体内容
    return (
      <NavLink activeClassName="selected" className="list-group-item" {...this.props} />
    )
  }
}
// 使用封装后的组件
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>

🔗 概念扩展children 是一个特殊的 prop 标签体内容会自动作为 children 属性传递。<MyNavLink to="/about">About</MyNavLink> 等价于 <MyNavLink to="/about" children="About"/>。所以 {...this.props} 展开时会自动包含 children


七、Switch 的使用(P81)

import { Switch, Route } from 'react-router-dom'

{/* 不用 Switch:path 匹配到后还会继续往下匹配 */}
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Route path="/home" component={Test} />  {/* Home 和 Test 都会展示! */}

{/* 用 Switch:匹配到第一个就停止 */}
<Switch>
  <Route path="/about" component={About} />
  <Route path="/home" component={Home} />
  <Route path="/home" component={Test} />  {/* 不会展示,因为上面已经匹配到了 */}
</Switch>

Switch 的作用:当匹配到第一个对应的路由后,就不再继续匹配了。提高效率,避免重复渲染。

💡 通常情况下,path 和 component 是一一对应的关系,所以 Switch 几乎是必用的。


八、解决样式丢失问题(P82)

当路由路径是多级结构时(如 /atguigu/about),刷新页面可能导致 CSS 样式丢失。

原因:多级路径刷新时,浏览器会把路径的前缀当作请求 CSS 的路径,导致请求到错误的资源。

三种解决方案

<!-- 方案1:CSS 引用路径不写 ./ 写 / (推荐) -->
<link rel="stylesheet" href="/css/bootstrap.css">

<!-- 方案2:CSS 引用路径用 %PUBLIC_URL%(仅适用于 CRA) -->
<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">

<!-- 方案3:使用 HashRouter(hash 值不会影响资源请求路径) -->

九、路由的模糊匹配与严格匹配(P83)

9.1 模糊匹配(默认)

{/* Link 的 to 是 /home/a/b */}
<Link to="/home/a/b">Home</Link>

{/* Route 的 path 是 /home → 能匹配!(模糊匹配:从头开始匹配,只要前缀对就行) */}
<Route path="/home" component={Home} />

{/* 但反过来不行 */}
<Link to="/home">Home</Link>
<Route path="/home/a/b" component={Home} />  {/* ❌ 匹配不上 */}

模糊匹配规则:Link 给出的路径必须包含 Route 的 path,且顺序一致(从头开始匹配)。

9.2 严格匹配

{/* exact={true} 或简写 exact 开启严格匹配 */}
<Route exact path="/home" component={Home} />

⚠️ 注意:严格匹配不要随便开启!有时候开启会导致无法匹配二级路由。需要时再开,不要提前开。


十、Redirect 的使用(P84)

import { Route, Switch, Redirect } from 'react-router-dom'

<Switch>
  <Route path="/about" component={About} />
  <Route path="/home" component={Home} />
  {/* 当所有路由都匹配不上时,重定向到 /about */}
  <Redirect to="/about" />
</Switch>

Redirect 的作用:写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到 Redirect 指定的路由。常用于设置默认页面。


十一、嵌套路由(P85)

// Home 组件中注册二级路由
export default class Home extends Component {
  render() {
    return (
      <div>
        <h3>我是 Home 的内容</h3>
        <div>
          <ul className="nav">
            <MyNavLink to="/home/news">News</MyNavLink>
            <MyNavLink to="/home/message">Message</MyNavLink>
          </ul>
          <Switch>
            {/* 二级路由的 path 必须带上一级路由的前缀 */}
            <Route path="/home/news" component={News} />
            <Route path="/home/message" component={Message} />
            <Redirect to="/home/news" />
          </Switch>
        </div>
      </div>
    )
  }
}

嵌套路由要点

  1. 注册子路由时要写上父路由的 path 值(如 /home/news
  2. 路由的匹配是按照注册路由的顺序进行的
  3. 这就是为什么不能随便开启严格匹配 — 如果 /home 开了严格匹配,/home/news 就匹配不到 Home 组件了

十二、向路由组件传递参数(P86-P89)

12.1 params 参数(P86)

// 路由链接(携带参数)
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>
  {msgObj.title}
</Link>

// 注册路由(声明接收)
<Route path="/home/message/detail/:id/:title" component={Detail} />

// Detail 组件中接收
const { id, title } = this.props.match.params

特点:参数在 URL 中可见,刷新不丢失。

12.2 search 参数(P87)

// 路由链接(携带参数)
<Link to={`/home/message/detail?id=${msgObj.id}&title=${msgObj.title}`}>
  {msgObj.title}
</Link>

// 注册路由(无需声明接收,正常注册即可)
<Route path="/home/message/detail" component={Detail} />

// Detail 组件中接收
import qs from 'querystring'
const { search } = this.props.location  // "?id=01&title=消息1"
const { id, title } = qs.parse(search.slice(1))  // 去掉开头的 ?

特点:参数在 URL 中可见,刷新不丢失。无需在 Route 中声明。

12.3 state 参数(P88)

// 路由链接(携带参数)
<Link to={{
  pathname: '/home/message/detail',
  state: { id: msgObj.id, title: msgObj.title }
}}>
  {msgObj.title}
</Link>

// 注册路由(无需声明接收)
<Route path="/home/message/detail" component={Detail} />

// Detail 组件中接收
const { id, title } = this.props.location.state || {}

特点:参数不在 URL 中显示(更隐蔽),BrowserRouter 刷新不丢失(因为 history 对象维护着 state),但 HashRouter 刷新会丢失。

12.4 三种参数对比(P89)

路由参数传递方式对比:
┌────────────┬──────────────────┬──────────────┬──────────────┐
│            │ params            │ search       │ state        │
├────────────┼──────────────────┼──────────────┼──────────────┤
│ URL 可见    │ ✅ 是             │ ✅ 是        │ ❌ 否         │
│ Route 声明  │ 需要 /:id/:title │ 不需要       │ 不需要        │
│ 接收方式    │ match.params     │ location     │ location     │
│            │                  │ .search      │ .state       │
│ 刷新保留    │ ✅               │ ✅           │ ✅ Browser    │
│            │                  │              │ ❌ Hash       │
└────────────┴──────────────────┴──────────────┴──────────────┘

十三、push 与 replace(P90)

{/* 默认是 push 模式:压入历史记录栈,可以回退 */}
<Link to="/home/message/detail">详情</Link>

{/* replace 模式:替换当前历史记录,不能回退 */}
<Link replace to="/home/message/detail">详情</Link>

push:留下历史痕迹,可以后退 replace:不留历史痕迹,替换当前记录


十四、编程式路由导航(P91)

不通过 <Link><NavLink>,而是通过代码实现路由跳转:

// push 跳转 + 携带 params 参数
this.props.history.push(`/home/message/detail/${id}/${title}`)

// replace 跳转 + 携带 params 参数
this.props.history.replace(`/home/message/detail/${id}/${title}`)

// push + search 参数
this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)

// push + state 参数
this.props.history.push('/home/message/detail', { id, title })

// 前进、后退
this.props.history.goBack()
this.props.history.goForward()
this.props.history.go(-2)  // 后退两步

使用场景:按钮点击跳转、定时跳转、条件判断后跳转等。


十五、withRouter(P92)

问题:一般组件(非路由组件)的 props 中没有 historylocationmatch 这三个路由对象,无法使用编程式导航。

解决:用 withRouter 高阶组件包裹:

import { withRouter } from 'react-router-dom'

class Header extends Component {
  back = () => {
    this.props.history.goBack()
  }

  render() {
    return (
      <div>
        <h1>React Router Demo</h1>
        <button onClick={this.back}>后退</button>
      </div>
    )
  }
}

// withRouter 可以加工一般组件,让其拥有路由组件特有的 API
export default withRouter(Header)

🔗 概念扩展:withRouter 是一个高阶组件(HOC) 高阶组件是一个函数,接收一个组件,返回一个新的增强组件。withRouter 的作用就是把 history、location、match 注入到一般组件的 props 中。


十六、BrowserRouter 与 HashRouter(P93)

BrowserRouter vs HashRouter:
┌──────────────────┬─────────────────────┬─────────────────────┐
│                  │ BrowserRouter        │ HashRouter           │
├──────────────────┼─────────────────────┼─────────────────────┤
│ 底层原理          │ H5 history API      │ URL 的 hash 值       │
│ URL 表现          │ /home               │ /#/home              │
│ 刷新对 state 影响  │ 无影响              │ state 参数会丢失      │
│ 兼容性            │ IE9 以下不兼容       │ 兼容性好              │
│ 服务器配置        │ 需要配合(404问题)   │ 不需要                │
└──────────────────┴─────────────────────┴─────────────────────┘

推荐:BrowserRouter ✅(URL 更美观,功能更完整)

🎯 面试高频:BrowserRouter 和 HashRouter 的区别?

  1. 底层原理不同:BrowserRouter 用 H5 history API,HashRouter 用 URL hash
  2. URL 表现不同:BrowserRouter 没有 #,HashRouter 有 #
  3. 刷新影响不同:BrowserRouter 的 state 参数刷新不丢失,HashRouter 会丢失
  4. HashRouter 可以用于解决一些路径错误相关的问题(如样式丢失)

本章知识图谱

React Router 5
├── 基础概念
│   ├── SPA:单页面应用,局部更新
│   ├── 前端路由:path → component
│   └── 原理:History API / Hash
├── 核心组件
│   ├── BrowserRouter / HashRouter → 包裹应用
│   ├── Link / NavLink → 路由链接
│   ├── Route → 注册路由
│   ├── Switch → 单一匹配
│   └── Redirect → 兜底重定向
├── 路由进阶
│   ├── 嵌套路由:子路由带父路由前缀
│   ├── 模糊匹配 vs 严格匹配(exact)
│   └── 样式丢失:/ 或 %PUBLIC_URL%
├── 路由参数
│   ├── params:URL 可见,Route 需声明
│   ├── search:URL 可见,无需声明
│   └── state:URL 不可见,Hash 刷新丢失
├── 编程式导航
│   ├── push / replace / goBack / goForward / go
│   └── withRouter:让一般组件也能用路由 API
└── 路由模式
    ├── BrowserRouter:推荐,URL 美观
    └── HashRouter:兼容性好,有 # 号

📌 下一篇:[React全家桶笔记(七):React UI组件库 — Ant Design实践] 将学习如何使用 Ant Design 组件库快速搭建美观的 React 应用。