基于 react 18.0.2、create-react-app 5.0.1、react router 5 讲解。
所以我们安装的时候 npm i react-router-dom@5
1. 相关理解
1.1 SPA 的理解
- 单页 Web 应用(single page web application,SPA)。
- 整个应用只有一个完整的页面。
- 点击页面中的链接不会刷新页面,只会做页面的局部更新。
- 数据都需要通过 ajax 请求获取, 并在前端异步展现。
1.2 路由的解释
1.2.1 什么是路由
- 一个路由就是一个映射关系(key:value)
- key 为路径, value 可能是 function 或 component
1.2.2 路由分类
后端路由:
-
理解: value 是 function, 用来处理客户端提交的请求。
-
注册路由:
router.get(path, function(req, res))
-
工作过程:当 node 接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
app.get("/search/users", function (req, res) {
const { q } = req.request;
axios({
url: "https://api.github.com/search/users",
params: { q },
}).then((response) => {
res.json(response.data);
});
});
前端路由:
- 浏览器端路由,value 是 component,用于展示页面内容。
- 注册路由:
<Route path="/test" component={Test}>
- 工作过程:当浏览器的 path 变为 /test 时, 当前路由组件就会变为 Test组 件
1.3 前端路由的基石
前端路由其实是基于 history 对象实现的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>前端路由的基石_history</title>
</head>
<body>
<a href="http://www.atguigu.com" onclick="return push('/test1') ">push test1</a>
<br/><br/>
<button onClick="push('/test2')">push test2</button>
<br/><br/>
<button onClick="replace('/test3')">replace test3</button>
<br/><br/>
<button onClick="back()"><= 回退</button>
<button onClick="forword()">前进 =></button>
<script
type="text/javascript"
src="https://cdn.bootcss.com/history/4.7.2/history.js"
>
</script>
<script type="text/javascript">
let history = History.createBrowserHistory() //方法一,直接使用H5推出的history身上的API
// let history = History.createHashHistory(); //方法二,hash值(锚点)
function push(path) {
// 添加进历史记录
history.push(path);
// 阻止默认行为,禁止 a 标签跳转
return false;
}
function replace(path) {
history.replace(path);
}
function back() {
history.goBack();
}
function forword() {
history.goForward();
}
history.listen((location) => {
console.log("请求路由路径变化了", location);
});
</script>
</body>
</html>
1.4 react-router-dom 的理解
react-router 库有三种实现,web、native(原生应用开发)、any (都可以使用),我们开发 web 一般使用 web,所以我们学习 react-router-dom
- react 的一个插件库。
- 专门用来实现一个 SPA 应用。
- 基于 react 的项目基本都会用到此库。
2. 基本路由使用
2.1 效果
2.2 准备
- 下载 react-router-dom:
npm install --save react-router-dom@5
- 引入 bootstrap.css:
<link rel="stylesheet" href="/css/bootstrap.css">
2.3 例子
index.js
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<React.StrictMode>
{/* 也可以使用 HashRouter */}
<BrowserRouter>
<App/>
</BrowserRouter>,
</React.StrictMode>);
App.jsx
export default class App extends Component {
render() {
return (<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 原生html中,靠<a>跳转不同的页面 */}
{/* <a className="list-group-item" href="./about.html">About</a>
<a className="list-group-item active" href="./home.html">Home</a> */}
{/* 在React中靠路由链接实现切换组件--编写路由链接 */}
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
{/* 若为 HashRouter,则为 # 开头 */}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</div>
</div>
</div>
</div>
</div>)
}
}
About/index.jsx
export default class About extends Component {
render() {
return (<h3>我是About的内容</h3>)
}
}
Home/index.jsx
export default class Home extends Component {
render() {
return (<h3>我是Home的内容</h3>)
}
}
2.4 总结
-
明确好界面中的导航区、展示区
-
导航区的 a 标签改为 Link 标签
<Link to="/xxxxx">Demo</Link>
-
展示区写 Route 标签进行路径的匹配
<Route path='/xxxx' component={Demo}/>
-
<App>
的最外侧包裹了一个<BrowserRouter>
或<HashRouter>
3. 路由组件和一般组件
- 写法不同
- 一般组件:
<Demo/>
- 路由组件:
<Route path="/demo" component={Demo}/>
- 一般组件:
- 存放位置不同
- 一般组件:components
- 路由组件:pages
- 接收到的 props 不同:
- 一般组件:写组件标签时传递了什么,就能收到什么
- 路由组件:接收到三个固定的属性
- history:
- go: ƒ go(n)
- goBack: ƒ goBack()
- goForward: ƒ goForward()
- push: ƒ push(path, state)
- replace: ƒ replace(path, state)
- location:
- pathname: “/about”
- search: “”
- state: undefined
- match:
- params: {}
- path: “/about”
- url: “/about”
- history:
4. react-router-dom 相关 API
4.1 内置组件
<BrowserRouter>
<HashRouter>
<Route>
<Redirect>
<Link>
<NavLink>
<Switch>
4.2 其它
- history 对象
- match 对象
- withRouter 函数
4.3 内置组件详解
4.3.1 Link
基本的路由组件,可以做到跳转作用。
4.3.2 NavLink
Link 的升级版,被点击的组件会自动添加 active(class),可以通过 activeClassName 指定激活时所添加的 class
<NavLink activeClassName="active" className="list-group-item"
to="/about">About</NavLink>
<NavLink activeClassName="active" className="list-group-item"
to="/home">Home</NavLink>
-
如果感觉每个 NavLink 都需要写 activeClassName 比较麻烦,可以自己封装一下 NavLink,封装公共属性。
export default class MyNavLink extends Component { render() { // console.log(this.props); // 不用写标签体,this.props 会将 children 解析出来。 return (<NavLink activeClassName="active" className="list-group-item" {...this.props}/>) } }
标签体的内容怎么传递?
标签体内容也是特殊的标签属性,标签体内容会自动传递到 props 的 children 属性。
4.3.4 Switch
当我们有一条路径存在两个匹配组件的时候,两个组件都会被展示。
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/home" component={Test}/>
Switch可以提高路由匹配效率(单一匹配),匹配上后就不继续匹配。
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/home" component={Test}/>
</Switch>
4.3.4 Redirect
可以用来重定向到路由
-
一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由
-
具体编码:
<Switch> <Route path="/about" component={About}/> <Route path="/home" component={Home}/> <Redirect to="/about"/> </Switch>
4.4 BrowserRouter 与 HashRouter 的区别
-
底层原理不一样:
- BrowserRouter 使用的是 H5 的 history API,不兼容 IE9 及以下版本。
- HashRouter使用的是 URL 的哈希值。
-
path 表现形式不一样
- BrowserRouter 的路径中没有 #,例如:
localhost:3000/demo/test
- HashRoute r的路径包含 #,例如:
localhost:3000/#/demo/test
- BrowserRouter 的路径中没有 #,例如:
-
刷新后对路由 state 参数的影响
- BrowserRouter 没有任何影响,因为 state 保存在 history 对象中。
- HashRouter 刷新后会导致路由 state 参数的丢失!!!
4.备注:HashRouter 可以用于解决一些路径错误相关的问题。
5. 样式丢失问题
public 目录对应服务地址的根目录。
我们在 index.html 中引入 <link rel="stylesheet" href="./css/bootstrap.css">
是没有问题的,当我们进入到路由组件 <Route path="/atguigu/about" component={About}/>
的时候也是没问题的,因为界面刷新是 ajax 局部刷新,页面并没有刷新,所以样式没有丢失。
但是当我们位于 /atguigu/about
刷新页面的时候样式就会丢失,因为当前路径下对应的 .
没有 bootstrap.css 文件,所以会丢失。
有以下解决方案
-
public/index.html 中 引入样式时不写
./
写/
(常用) -
public/index.html 中 引入样式时不写
./
写%PUBLIC_URL%
(常用) -
使用 HashRouter
- 使用 HashRouter 时路径为
http://localhost:3000/#/atguigu/home
, # 号后面为前端资源,不带给服务器,我们刷新界面时服务器路径还是http://localhost:3000
,对应的还是 public 目录。
- 使用 HashRouter 时路径为
6. 路由匹配
路由匹配分为严格匹配和模糊匹配。
1.默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
2.开启严格匹配:<Route exact={true} path="/about" component={About}/>
3.严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由
我们看个例子,这个例子中 Home 组件能成功展示,因为使用了模糊匹配。
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home/a/b">Home</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</Switch>
</div>
</div>
</div>
</div>
- 但是
/a/home/b
无法匹配,因为顺序不一致
我们添加 exact 后便无法匹配 Home 组件
<Route exact path="/home" component={Home}/>
7. 嵌套路由使用
-
注册子路由时要写上父路由的 path 值
-
路由的匹配是按照注册路由的顺序进行的
7.1 效果
7.2 例子
App.js
export default class App extends Component {
render() {
return (<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<Header/>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 在React中靠路由链接实现切换组件--编写路由链接 */}
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由 */}
<Switch>
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Redirect to="/about"/>
</Switch>
</div>
</div>
</div>
</div>
</div>)
}
}
pages/Home/index.jsx
import React, {Component} from 'react'
import MyNavLink from '../../components/MyNavLink'
import {Route, Switch, Redirect} from 'react-router-dom'
import News from './News'
import Message from './Message'
export default class Home extends Component {
render() {
return (<div>
<h3>我是Home的内容</h3>
<div>
<ul className="nav nav-tabs">
<li>
<MyNavLink to="/home/news">News</MyNavLink>
</li>
<li>
<MyNavLink to="/home/message">Message</MyNavLink>
</li>
</ul>
{/* 注册路由 */}
<Switch>
<Route path="/home/news" component={News}/>
<Route path="/home/message" component={Message}/>
<Redirect to="/home/news"/>
</Switch>
</div>
</div>)
}
}
pages/Home/News/index.jsx
import React, {Component} from 'react'
export default class News extends Component {
render() {
return (<ul>
<li>news001</li>
<li>news002</li>
<li>news003</li>
</ul>)
}
}
8. 向路由组件传递参数数据
8.1 效果
8.2 params 参数
-
路由链接(携带参数):
<Link to='/demo/test/tom/18'}>详情</Link>
-
注册路由(声明接收):
<Route path="/demo/test/:name/:age" component={Test}/>
-
接收参数:this.props.match.params
export default class Message extends Component {
state = {
messageArr: [{id: '01', title: '消息1'}, {id: '02', title: '消息2'},
{id: '03', title: '消息3'},]
}
render() {
const {messageArr} = this.state
return (<div>
<ul>
{messageArr.map((msgObj) => {
return (<li key={msgObj.id}>
{/* 向路由组件传递params参数 */}
<Link
to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
</li>)
})}
</ul>
<hr/>
{/* 声明接收params参数 */}
<Route path="/home/message/detail/:id/:title" component={Detail}/>
</div>)
}
}
const DetailData = [{id: '01', content: '你好,中国'},
{id: '02', content: '你好,尚硅谷'}, {id: '03', content: '你好,未来的自己'}]
export default class Detail extends Component {
render() {
console.log(this.props);
// 接收params参数
const {id, title} = this.props.match.params
const findResult = DetailData.find((detailObj) => {
return detailObj.id === id
})
return (<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>)
}
}
8.3 search 参数
-
路由链接(携带参数):
<Link to='/demo/test?name=tom&age=18'}>详情</Link>
-
注册路由(无需声明,正常注册即可):
<Route path="/demo/test" component={Test}/>
-
接收参数:this.props.location.search
-
备注:获取到的 search 是 urlencoded 编码字符串,需要借助 querystring 解析
export default class Message extends Component {
state = {
messageArr: [{id: '01', title: '消息1'}, {id: '02', title: '消息2'},
{id: '03', title: '消息3'},]
}
render() {
const {messageArr} = this.state
return (<div>
<ul>
{messageArr.map((msgObj) => {
return (<li key={msgObj.id}>
{/* 向路由组件传递search参数 */}
<Link
to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
</li>)
})}
</ul>
<hr/>
{/* search参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>
</div>)
}
}
const DetailData = [{id: '01', content: '你好,中国'},
{id: '02', content: '你好,尚硅谷'}, {id: '03', content: '你好,未来的自己'}]
export default class Detail extends Component {
render() {
console.log(this.props);
// 接收search参数
const {search} = this.props.location
const {id, title} = qs.parse(search.slice(1))
const findResult = DetailData.find((detailObj) => {
return detailObj.id === id
})
return (<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>)
}
}
8.4 state 参数
-
路由链接(携带参数):
<Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情</Link>
-
注册路由(无需声明,正常注册即可):
<Route path="/demo/test" component={Test}/>
-
接收参数:this.props.location.state
-
备注:state 传递参数不会在地址栏显示,刷新也可以保留住参数
export default class Message extends Component {
state = {
messageArr: [{id: '01', title: '消息1'}, {id: '02', title: '消息2'},
{id: '03', title: '消息3'},]
}
render() {
const {messageArr} = this.state
return (<div>
<ul>
{messageArr.map((msgObj) => {
return (<li key={msgObj.id}>
{/* 向路由组件传递state参数 */}
<Link to={{
pathname: '/home/message/detail',
state: {id: msgObj.id, title: msgObj.title}
}}>{msgObj.title}</Link>
</li>)
})}
</ul>
<hr/>
{/* state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>
</div>)
}
}
const DetailData = [{id: '01', content: '你好,中国'},
{id: '02', content: '你好,尚硅谷'}, {id: '03', content: '你好,未来的自己'}]
export default class Detail extends Component {
render() {
console.log(this.props);
// 接收state参数
const {id, title} = this.props.location.state || {}
const findResult = DetailData.find((detailObj) => {
return detailObj.id === id
}) || {}
return (<ul>
<li>ID:{id}</li>
<li>TITLE:{title}</li>
<li>CONTENT:{findResult.content}</li>
</ul>)
}
}
为什么刷新界面 state 不会丢失?
因为 history 包含 location,刷新界面 history 还在,location 不会丢失,所以 state 还在。
9. 编程式路由导航
9.1 效果
9.2 路由跳转的方式
有 push 和 replace 两种模式,默认是 push。
replace 的开启方式
<Link replace to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{*msgObj*.title}</Link>
9.3 history api
借助 this.prosp.history 对象上的 API 对操作路由跳转、前进、后退
-
this.prosp.history.push()
-
this.prosp.history.replace()
-
this.prosp.history.goBack()
-
this.prosp.history.goForward()
-
this.prosp.history.go()
9.4 例子
export default class Message extends Component {
state = {
messageArr: [{id: '01', title: '消息1'}, {id: '02', title: '消息2'},
{id: '03', title: '消息3'},]
}
replaceShow = (id, title) => {
//replace跳转+携带params参数
//this.props.history.replace(`/home/message/detail/${id}/${title}`)
//replace跳转+携带search参数
// this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)
//replace跳转+携带state参数
this.props.history.replace(`/home/message/detail`, {id, title})
}
pushShow = (id, title) => {
//push跳转+携带params参数
// this.props.history.push(`/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})
}
back = () => {
this.props.history.goBack()
}
forward = () => {
this.props.history.goForward()
}
go = () => {
this.props.history.go(-2)
}
render() {
const {messageArr} = this.state
return (<div>
<ul>
{messageArr.map((msgObj) => {
return (<li key={msgObj.id}>
{/* 向路由组件传递state参数 */}
<Link to={{
pathname: '/home/message/detail',
state: {id: msgObj.id, title: msgObj.title}
}}>{msgObj.title}</Link>
<button
onClick={() => this.pushShow(msgObj.id, msgObj.title)}>push查看
</button>
<button onClick={() => this.replaceShow(msgObj.id,
msgObj.title)}>replace查看
</button>
</li>)
})}
</ul>
<hr/>
{/* state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>
<button onClick={this.back}>回退</button>
<button onClick={this.forward}>前进</button>
<button onClick={this.go}>go</button>
</div>)
}
}
10. withRouter
还是以上面例子为例。
当我们想把按钮转移到 Header 中时,会发现 header 获取不到 this.props.history 对象,因为 Header 非路由组件,这时候可以使用 withRouter 解决。
export default withRouter(Header)
withRouter 可以加工一般组件,让一般组件具备路由组件的所有特有 API