React(七)- React路由以及嵌套路由的使用
前言
在讲React路由前,先来看下SPA的相关概念:
- SPA:
single page web application
单页Web应用。 - 整个应用只有一个完整的页面。
- 点击页面中的链接不会刷新页面,只会做页面的局部更新。
- 数据都需要通过Ajax请求获取,并在前端异步展示。
一. 路由
路由的概念:
- 一个路由就是一个映射关系(
key:value
)。 key
是路径,value
可能是一个组件或者一个函数。
路由的分类:
后端路由:value
是Function
,用来处理客户端提交的请求。
- 注册路由:
router.get(path,function(req,res))
- 工作流程:当node接收到一个请求的时候,会根据请求路径来找到匹配的路由,调用路由中的函数来处理请求,返回相应数据。
前端路由:value
是Component
组件,用来展示页面内容。
- 注册路由:
< Route path="/test" component={Test} >
- 工作流程:当浏览器的
path
变为/test
的时候,当前路由组件就会变成为Test
组件。
1.1 路由的使用
1.安装对应插件react-router-dom
:
npm install react-router-dom
2.对应项目结构如下:bootstrap.css文件下载地址:提取码:eam4
index.html
文件中引入对应的css
文件:
<link rel="stylesheet" href="./bootstrap.css">
About组件:
import React, { Component } from 'react'
export default class About extends Component {
render() {
return (
<div>
<h2>About</h2>
</div>
)
}
}
Home组件:
import React, { Component } from 'react'
export default class Home extends Component {
render() {
return (
<div>
<h2>Home</h2>
</div>
)
}
}
App组件:
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'
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">
{/**注册路由 */}
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
</div>
</div>
</div>
</div>
</div>
)
}
}
入口文件index.js:
// 引入React核心库
import React from 'react'
// 引入ReactDOM
import ReactDOM from 'react-dom'
// 引入App组件
import App from './App'
import { BrowserRouter } from 'react-router-dom'
// 渲染App到页面
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
页面效果图如下:
点击About标签后:页面并不会刷新
通过上述的案例,做出如下总结:
- 我们要明确好界面中的导航区和展示区。
- 导航区的
a
标签改为Link
标签:
<Link to="/xxx">Demo</Link>
- 展示区和
Route
标签进行路径的匹配:
<Route path="/xxx" component={组件} />
- 要想更加便利的添加一个路由,可将
BrowserRouter
标签或者HashRouter
标签放到App
组件的外侧,用于管理整个组件的路由。
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
可见,上述的案例用的是BrowserRouter
,那么如果改为HashRouter
会有什么样的效果呢?只需要修改入口文件:
页面效果如下(只关注地址栏):
可以发现,跟BrowserRouter
相比,地址栏中多了一个“#”号,两者的区别在后文会详细介绍。
1.2 路由组件和一般组件的区别
案例:我们将上述案例中的提示语单独作为一个Header
组件来看看:
import React, { Component } from 'react';
class Header extends Component {
render() {
return (
<div className="page-header"><h2>React Router Demo</h2></div>
);
}
}
export default Header;
对应App.jsx文件:改动部分:
import Header from './components/Header'
///....
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<Header />
</div>
</div>
打开控制台,查看对应组件中的属性,Header
组件:
About
组件:
可以发现,About
组件的props
属性中含多个键值对,基于此,可以理解为:
About
组件:就是路由组件。Header
组件:就是一般组件。
那么两者有什么区别呢?
- 写法不同
- 一般组件:
< Demo/>
- 路由组件:
< Route path="/demo" component={Demo} >
- 存放位置不同
- 一般组件:
components
目录 - 路由组件:
pages
目录
- 接收到的
props
不同
- 一般组件:写组件标签的时候传递了什么,就能收到什么对象/值/函数等。
- 路由组件:会接收到三个固定的属性
属性包含:
1.3 NavLink的使用
上面的案例在页面显示的时候,可以发现一个问题:在点击对应标签的时候,并不会做一个明显的高亮显示,如图:
那么该如何进行优化呢?最简单的方法,将Link
标签改为NavLink
,代码修改如下:
import { NavLink, Route } from 'react-router-dom'
<NavLink className="list-group-item" to="/about">About</NavLink>
<NavLink className="list-group-item" to="/home">Home</NavLink>
此时的页面效果:
若想更改高亮的颜色,那怎么办呢?
对应NavLink
标签修改:增加一个activeClassName
<NavLink activeClassName="myHighLight" className="list-group-item" to="/about">About</NavLink>
<NavLink activeClassName="myHighLight" className="list-group-item" to="/home">Home</NavLink>
index.html
文件中添加样式:!important
代表将当前的样式优先级调成最高。
<style>
.myHighLight{
background-color: brown !important;
color: cornsilk !important;
}
</style>
页面效果如下:
问题来了,如果说导航栏比较多,那么对于上述的更改,岂不是每个标签都得加上activeClassName
?这样会显得代码很冗余。
因此,我们需要通过封装NavLink
组件来解决上述问题。
1.3.1 封装NavLink组件
定义一个MyNavLink
组件:
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom'
class MyNavLink extends Component {
render() {
return (
<NavLink activeClassName="myHighLight" className="list-group-item" {...this.props}>{this.props.children}</NavLink>
);
}
}
export default MyNavLink;
App
组件中:
<MyNavLink to="/home" a={1} b={2}>Home</MyNavLink>
<MyNavLink to="/about" >About</MyNavLink >
Tip:
- 往
MyNavLink
标签中添加的属性,都会保存到其实例对象的props
属性中。 - 而我们可以通过语法糖:
{....this.props}
的方式对其属性进行一一的赋值。 - 因此对于这种自定义的组件,我们依旧可以按需来传递参数(比如路由A需要3个参数,而路由B则不需要参数)
总结:
- NavLink可以实现路由链接的高亮,通过activeClassName指定样式名称。
- 标签体内容是一个特殊的标签属性。(这里的标签体内容指的是
< div >xxx< /div >
中的xxx
) - 通过
this.props.children
可以获得标签体内容。
1.3.2 通过Switch来停止路由匹配
在上述案例的基础上,随意添加一个Test组件:
import React, { Component } from 'react';
class Test extends Component {
render() {
return (
<div>
<h2>Test</h2>
</div>
);
}
}
export default Test;
``
App组件中添加Test组件:
```javascript
import Test from './components/Test'
并添加一条新的路由:
那么这种情况下,会发生什么,可见,React将以/home
为path
的组件全部渲染了。意思就是说,从上到下按照顺序,即使匹配到了对应的路由,但是依旧会往下继续匹配。
那么这种问题该如何解决?只需要在路由标签的外侧,包一层Switch
标签即可。
import { NavLink, Route, Switch } from 'react-router-dom'
对应的页面效果:可见匹配到对应的路由后,即停止向下匹配。
对于Switch
的使用作出总结:
- 通常情况下,
path
和component
是一种一一对应的关系。 - Switch可以提高路由匹配的效率(因为是单一匹配)
1.3.3 NavLink样式丢失问题
大家有没有发现,上述案例中的路由地址都是一级的,啥意思呢?就是说路由都是"/xxx"
,而不是"/xxx/xxx"
,那么如果我将路由地址进行修改,会发生什么?
看似没什么问题:
但是如果页面进行刷新,可以发现,样式丢失了。
原因是什么?大家还记得文章上面在public/css
目录下引入了一个bootstrap.css
样式吗?若路由地址是"/xxx"
,那么在页面刷新的时候,会重新对所需的样式发起请求,地址如下:(正确的地址,因为public
目录是启动的服务器中内置的一个根目录,其下面的文件可以直接在URL中访问)
http://localhost:3000/css/bootstrap.css
但是如果路由地址是"/xxx"/xxx
,那么对应发起请求的地址是:(错误的地址)
http://localhost:3000/xxx/css/bootstrap.css
也因此,发生了样式丢失的问题,解决方案1如下:修改index.html
中的引入。
修改后:去掉 ./ 则代表去localhost:3000下的css目录下去寻找
<link rel="stylesheet" href="/css/bootstrap.css">
修改前:./ 代表以当前路径出发去寻找对应的路径
<link rel="stylesheet" href="./css/bootstrap.css">
解决方案2如下:
<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css">
解决方案3如下:将BrowserRouter
改为HashRouter
:
那么无论页面刷新多少次,样式都不会丢失。
对于以上案例,总结如下:
public/index.html
中引入样式的时候不写./
,一般写/
。public/index.html
中引入样式的时候不写./
,一般写%PUBLIC_URL%
。- 使用
HashRouter
。
总结☆
根据以上内容,我们可以发现,React中使用路由的时候需要注意这么几个点:
- 通过
< Route path="/demo" component={Demo} >
来定义对应的路由应该渲染哪个组件。通过< Link to="/xxx">
或者< NavLink to="/xxx">
来定义路由。 - 最好在使用路由的时候,外层包一个Switch标签,保证路由的单一匹配。
- 对于
index.html
文件可能存在的样式丢失问题,要么就规范index.html
文件中样式的引入地址,要么就用HashRouter
来代替BrowserRouter
。
1.4 路由的模糊匹配和严格匹配
案例1:
页面中访问/home
,看看是否能够匹配到:(可以)
案例2:
页面中访问/home
,看看是否能够匹配到:(不可以)
从该两个案例中,我们发现,React在默认的情况下,支持模糊查询,只要路由的前缀模糊匹配到了即可配对成功。
那么如何进行严格匹配呢?做出以下修改即可:(或者直接加上exact
即可,后面的={true}
可以去掉)
页面中访问/home
,看看是否能够匹配到:(不可以)
该小节做出以下总结:
- React路由默认使用的是模糊匹配。(顺序要一致)
- 开启严格匹配的方式:
< Route exact path="/about" component={About} />
- 严格匹配不要随意开启,有些时候开启会导致二级路由无法匹配。
1.5 Redirect的使用
需求:页面打开的时候就默认匹配某个路由,然后渲染对应的组件。
方案:使用Redirect
标签:
import { Route, Switch, Redirect } from 'react-router-dom'
<Switch>
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Redirect to="/about" />
</Switch>
当页面打开时:地址默认为/about
,并且渲染对应组件内容。
注意:
- 一般
Redirect
写在所有路由注册的最下方,当所有路由都没有匹配到的时候,默认跳转Redirect
指定的路由。
二. 嵌套路由
项目结构如下:
项目代码如下:(在原来的基础上新增两个子组件,处于Home
组件下)
Message组件:
import React, { Component } from 'react'
export default class Message extends Component {
render() {
return (
<div>
<ul>
<li>
<a href="/message1">message001</a>
</li>
<li>
<a href="/message2">message002</a>
</li>
<li>
<a href="/message/3">message003</a>
</li>
</ul>
</div>
)
}
}
News组件:
import React, { Component } from 'react'
export default class News extends Component {
render() {
return (
<ul>
<li>news001</li>
<li>news002</li>
<li>news003</li>
</ul>
)
}
}
Home组件:
import React, { Component } from 'react'
import News from './News'
import Message from './Message'
import MyNavLink from '../../components/MyNavLink'
import { Route, Switch, Redirect } from 'react-router-dom'
export default class Home extends Component {
render() {
return (
<div>
<h3>Home</h3>
<ul className="nav nav-tabs">
<li>
<MyNavLink to="/home/news">News</MyNavLink>
</li>
<li>
<MyNavLink to="/home/message">Message</MyNavLink>
</li>
</ul>
<Switch>
{/**注册子组件路由的时候,一定要写上父路由的path值 */}
<Route path="/home/news" component={News}></Route>
<Route path="/home/message" component={Message}></Route>
<Redirect to="/home/news" />
</Switch>
</div>
)
}
}
页面效果如下:
注意:
- 至于子组件为何要跟上父组件的
path
值,因为倘若不跟,那么其本质上是和最顶层的父组件App
下面定义的路由是同级关系,因此按照顺序进行路由匹配的时候,以/news
为例,倘若找不到,就会根据Redirect
指定的默认路由输出。那么这种情况,点击子导航News或者Message,最后页面呈现的都是About
组件的内容。 - 路由匹配的时候,会先从父类组件开始匹配。按照顺序。
其实该章节本质上就需要大家知道一点即可:
- React中若需要使用到嵌套路由,那么子组件中的路由前缀必须跟上父组件的路由。
2.1 向路由组件传递参数
2.1.1 传递params参数
希望在原本案例基础上再嵌套一层组件,效果如下:Detail
组件的内容根据标签的不同来显示不同的数据,那么这种情况肯定就需要传递对应的参数。
在Message
组件下创建一个子组件Detail
:
import React, { Component } from 'react';
const DetailData = [
{ id: '01', content: 'Hello1' },
{ id: '02', content: 'Hello2' },
{ id: '03', content: 'Hello3' },
]
class Detail extends Component {
render() {
// 接收params参数
const { id, title } = this.props.match.params
// 通过Id去寻找对应的content内容
const findResult = DetailData.find(dataObj => {
return dataObj.id == id
})
return (
<ul>
<li>Id:{id}</li>
<li>Title:{title}</li>
<li>Content:{findResult.content}</li>
</ul>
);
}
}
export default Detail;
Message
组件:
import React, { Component } from 'react'
import Detail from './Detail'
import { Link, Route } from 'react-router-dom'
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参数,同时不要忘了加上Detail所有父组件的路由前缀 */}
<Route path="/home/message/detail/:id/:title" component={Detail} />
</div>
)
}
}
至于Detail
组件中,为何params
参数的接收时候通过this.props.match.params
来获得呢?根据如下:(通过console.log(this.props.match.params)
观察数据)
可见,React
将路由的params
参数都保存到props.match.params
中了。
对应的URL地址:
总结1
- 路由的
params
参数指的是放在路由链接里面的参数,例如:
若参数是写死的:
<Link to='/home/message/detail/100'}>{msgObj.title}</Link>
若是动态的参数:需要加{},并且用``代替''
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
- 注册路由(声明参数)的写法:
< Route path="/home/message/detail/:id/:title" component={Detail} />
,对应的参数通过:xxx
的写法即可。 - 接收参数,通过
this.props.match.params
来获取。
2.1.2 传递search参数
改动如下:
Message
组件中:
路由的声明:
{/**向路由组件传递params参数 */}
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
{/**向路由组件传递search参数 */}
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
路由的接收及匹配:
{/**声明接收params参数 */}
<Route path="/home/message/detail/:id/:title" component={Detail} />
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
{/**声明接收search参数,无需携带参数 */}
<Route path="/home/message/detail" component={Detail} />
Detail
组件中:
const { id, title } = this.props.match.params
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
const { search } = this.props.location
// 因为这里获取到的search还带一个 ? ,所以需要通过slice方法来过滤第一个字符
import qs from 'querystring'
const { id, title } = qs.parse(search.slice(1))
证据如下:
对应的URL地址:
总结2
- 路由链接(携带参数):
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>
- 注册路由(无需声明参数,正常注册path即可):
< Route path="/home/message/detail" component={Detail} />
- 接收参数通过
this.props.location
来获取。 - 但是获取到的
search
对象是urlencoded
编码字符串,需要通过querystring
来解析:
import qs from 'querystring'
const { id, title } = qs.parse(search.slice(1))
2.1.3 传递state参数
大家可以发现,上面两种传递参数的方式,其参数都会暴露到URL中,那么为了防止暴露,可以通过state
来传递参数。
Message
组件改动:
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
<Link to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
对应的存储位置:
因此Detail
组件改动:
const { id, title } = this.props.match.params
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
const { id, title } = this.props.location.state
那么此时URL地址为:可以发现URL地址中并不会暴露相关的参数。
总结3
- 路由链接(携带参数):
<Link to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
- 注册路由(无需声明参数,正常注册path即可):
< Route path="/home/message/detail" component={Detail} />
- 接收参数通过
this.props.location.state
来获取。 - 备注:URL中不会暴露对应的参数,并且哪怕页面刷新,状态也不会丢失。
2.2 push和replace
默认路由的跳转,使用的是push
模式,也就是说,每次进行路由跳转的时候,都会留下痕迹。 即页面可以点击回退:
那么如何开启replace
模式,即替换站点,不留痕迹呢?解决:在Linke标签中添加一个属性replace
即可,如下:
验证如下:
点击消息1:
点击消息2:
点击页面回退:
此时页面展示为:
如果是push
模式则:
2.2.1 编程式路由导航以及withRouter的使用
需求效果如下:
Message
组件:(路由组件)
replaceShow = (id, title) => {
// replace跳转
this.props.history.replace(`/home/message/detail/${id}/${title}`)
}
pushShow = (id, title) => {
// push跳转
this.props.history.push(`/home/message/detail/${id}/${title}`)
}
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>
<button onClick={() => this.pushShow(msgObj.id, msgObj.title)}>push查看</button>
<button onClick={() => this.replaceShow(msgObj.id, msgObj.title)}>replace查看</button>
</li>
)
})
}
</ul>
<hr />
<Route path="/home/message/detail/:id/:title" component={Detail} />
</div>
)
}
Header
组件:(一般组件)
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom'
class Header extends Component {
back = () => {
this.props.history.goBack()
}
forward = () => {
this.props.history.goForward()
}
go = () => {
this.props.history.go(2)
}
render() {
return (
<div className="page-header">
<h2>React Router Demo</h2>
<button onClick={this.back}>回退</button>
<button onClick={this.forward}>前进</button>
<button onClick={this.go}>go</button>
</div>
);
}
}
export default withRouter (Header);
若页面效果呈现:
- 点击对应标签后的
push查看
按钮(页面留下访问痕迹),对应标签显示相应的内容。 - 点击
Header
组件中的go
、forward
、back
按钮分别会让页面前进至后2个历史记录、前进至后1个历史记录、倒退至上1个历史记录。
而诸如以下方式编程,就是所谓的编程式路由导航。
pushShow = (id, title) => {
// push跳转
this.props.history.push(`/home/message/detail/${id}/${title}`)
}
还注意,Header
身为一般组件,也就是说它并不具备路由组件应有的一些属性,比如操作this.props.history.goBack()
以及相关的方法,那么怎么办?
可见代码中,对于Header
这样的一般组件,引用了withRouter
,并在最后暴露对象的时候,使用export default withRouter (Header);
作用:
withRouter
可以加工一般组件,让一般组件具备路由组件所持有的API。withRouter
的返回值是一个新的组件,可以理解为将一般组件包装为路由组件。
2.3 BrowserRouter和HashRouter的区别
- 底层原理不一样:
BrowserRouter:使用H5的history API,不兼容IE9和以下版本。
HashRouter:使用的是URL的哈希值。
- URL表现形式不一样:
BrowserRouter:路径中没有#,例如localhost:3000/demo/test
HashRouter:路径中有#,例如localhost:3000/#demo/test
- 刷新后对路由
state
参数的影响:☆
BrowserRouter:没有任何影响,因为state保存在histroy对象中。
HashRouter:刷新后会导致路由state参数的丢失。
- HashRouter可以用于解决一些路径错误的相关问题。