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>
)
}
}
核心步骤:
- 用
<BrowserRouter>或<HashRouter>包裹整个应用 - 用
<Link to="/xxx">编写路由链接(替代<a>标签) - 用
<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>
)
}
}
嵌套路由要点:
- 注册子路由时要写上父路由的 path 值(如
/home/news) - 路由的匹配是按照注册路由的顺序进行的
- 这就是为什么不能随便开启严格匹配 — 如果
/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 中没有 history、location、match 这三个路由对象,无法使用编程式导航。
解决:用 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 的区别?
- 底层原理不同:BrowserRouter 用 H5 history API,HashRouter 用 URL hash
- URL 表现不同:BrowserRouter 没有 #,HashRouter 有 #
- 刷新影响不同:BrowserRouter 的 state 参数刷新不丢失,HashRouter 会丢失
- 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 应用。