React系列(五)——React路由
前言
路由是React项目中相当重要的概念,对于功能较为复杂的网页来说,必然会涉及到不同功能间的页面跳转,本篇文章将对React官方维护的路由库
React-Router-Dom
的使用和常用组件进行讲解,同时对路由组件传递param参数的方式进行讲解,希望对各位读者有所参考。
一、了解SAP和路由的概念
SAP(single page web application)的意思是单页Web应用,正如前言所说,一般来说功能较为复杂都会涉及到页面跳转的功能,而传统的前端页面跳转往往是利用<a/>
标签进行跳转,这种方式虽然可以实现功能,但是每次跳转到新的页面都会重新对页面的元素进行加载,这样其实对于用户来说是不太友好的。而单页Web应用则较好的解决了这个问题,因为SAP整个应用都是在一个页面上进行的,每次的页面跳转只涉及到页面中对应组件(模块)的更新操作,这样就在一定程度上让页面不需要加载重复的页面元素。
再说说路由
路由其实可以理解为是一个映射关系,即路径到组件或者函数的对应关系,比如说/home
这个路径对应着Home
这个首页组件,在React中,有react-router-dom
这个官方维护的组件库来帮助我们处理项目中的路由问题,需要注意的是,我们用create-react-dom
创建的react项目,默认是没有react-router-dom
的,所以需要我们自己再额外下载到项目中。
二、路由入门小案例
当前存在Home
、About
两个组件,我们希望在页面上的导航栏中实现点击对应标签,即可在页面上显示对应组件的内容。
Home组件:
import React, { Component } from 'react'
export default class Home extends Component {
render() {
return (
<h3>我是Home的内容</h3>
)
}
}
About组件:
import React, { Component } from 'react'
export default class About extends Component {
render() {
return (
<h3>我是About的内容</h3>
)
}
}
App组件
省略import...
export default class App extends Component {
render() {
return (
<div>
<div class="row">
<div class="col-xs-offset-2 col-xs-8">
<div class="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div class="row">
<div class="col-xs-2 col-xs-offset-2">
<div class="list-group">
<a class="list-group-item" to='/about'>About</a>
<a class="list-group-item" to='/home'>Home</a>
</div>
</div>
<div class="col-xs-6">
<div class="panel">
<div class="panel-body">
</div>
</div>
</div>
</div>
</div>
)
}
}
步骤一:下载路由组件库
install i react-router-dom
步骤二:引用Link标签,替代<a>
标签进行跳转:
需要注意的是<Link>其实本质上也是<a>
标签,只是说它阻止了a标签默认的跳转动作,但保留了其修改浏览器URL路径的能力。
import {Link,Router, Route} from 'react-router-dom'
...
export default class App extends Component {
render() {
return (
<div>
<div class="row">
<div class="col-xs-offset-2 col-xs-8">
<div class="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div class="row">
<div class="col-xs-2 col-xs-offset-2">
<div class="list-group">
{/*
使用Link标签代替 a 标签,这一步是用于实现第一步:浏览器URL的变更
*/}
<Link class="list-group-item" to='/about'>About</Link>
<Link class="list-group-item" to='/home'>Home</Link>
</div>
</div>
<div class="col-xs-6">
<div class="panel">
<div class="panel-body">
</div>
</div>
</div>
</div>
</div>
)
}
}
步骤三:定义路由对应要跳转的组件:
这一步也比较好理解,前端路由器(会在步骤四配置)会自动检测到浏览器的url发生了变化,此时就会拿新的url地址到路由表中进行匹配,步骤三定义的就是和url匹配成功的路由,将显示对应哪个组件。
import {Link,Router, Route} from 'react-router-dom';
export default class App extends Component {
render() {
return (
<div>
<div class="row">
<div class="col-xs-offset-2 col-xs-8">
<div class="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div class="row">
<div class="col-xs-2 col-xs-offset-2">
<div class="list-group">
{/*
使用Link标签代替 a 标签,这一步是用于实现第一步:浏览器URL的变更
*/}
<Link class="list-group-item" to='/about'>About</Link>
<Link class="list-group-item" to='/home'>Home</Link>
</div>
</div>
<div class="col-xs-6">
<div class="panel">
<div class="panel-body">
{/*
在App.js中,定义路由,路由规定了path对应跳转的组件
*/}
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
</div>
</div>
</div>
</div>
</div>
)
}
}
步骤四:在最外层的index.js中配置前端路由器
这里可以使用Router组件,但我们一般是使用功能更为强大的BrowerRouter或者HashRouter。
import React from 'react'
import ReactDOM from 'react-dom'
// 注意,我们一般不直接引入Router,而是引入 BrowserRouter 或者 HashRouter
import {BrowserRouter} from 'react-router-dom'
import App from './App'
ReactDOM.render(<BrowserRouter><App/></BrowserRouter>,document.getElementById('root'))
最后,我们再对React路由实现的原理做一个小结:
(1)使用<Link>等标签实现对浏览器path的操作(本质上是对BOM对象的history进行操作)
(2)当前端路由器检测到浏览器的path发生了变化,就会到路由中对新的路径进行匹配
三、路由的常用组件
(一)NavLink标签
NavLink组件是在Link组件的基础上做了高亮特效的增强,在我们快速入门的案例中,我们想要增加一个功能,当我点击导航栏的某个标签时,对应的标签要有高亮显示的效果。比较容易地我们会想到给每个标签增加一个是否高亮的flag,当满足选中的条件时,即将高亮的样式追加到页面的标签上。
而NavLink组件可以说是帮我们简化了上述的操作,我们只需要传入activeClassName指定具体追加的属性即可。需要注意的是,如果追加的高亮属性命名是 active时,可以省略。
上一小节的案例可以优化为:
import {NavLink,Route} from 'react-router-dom';
export default class App extends Component {
render() {
return (
<div>
...
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<NavLink activeClassName="active" className="list-group-item" to='/about'>About</NavLink>
<NavLink activeClassName="active" className="list-group-item" to='/home'>Home</NavLink>
</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>
)
}
}
(二)Switch组件
当路由中出现了2个或者2个以上的path同时匹配的情况,那么实际上对应的路由组件都会被渲染。如果我们想要说只渲染(挂载)第一个匹配上的组件的话,那么我们可以使用<Switch>组件来解决。
我们先来看一下未使用Switch组件的情况: /home
对应着2个组件
export default class App extends Component {
render() {
return (
<div>
...
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
<Route path="/about" component={About}/>
<Route path="/home" component={Home}/>
<Route path="/home" component={Home2}/>
</div>
</div>
</div>
</div>
</div>
)
}
}
我们可以引入Switch组件来解决这个问题:
export default class App extends Component {
render() {
return (
...
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<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}/>
<Route path="/home" component={Home2}/>
</Switch>
</div>
</div>
</div>
</div>
</div>
)
}
}
(三)Redirect组件
我们在进入网站的时候,网站的导航栏中往往会有一个默认选中的标签,对于这种场景,React路由其实也为我们提供了对应的组件来帮助我们简化开发,那就是Redirect组件。我们在快速入门的案例中新增加一个功能,进入首页后,默认跳转到/about
路径下。
export default class App extends Component {
render() {
return (
<div>
...
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<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>
)
}
}
四、精准匹配和模糊匹配
React路由的匹配方式一共有2种,分别是精准匹配和模糊匹配。
我们知道,要想正确使用react-router-dom来实现路由跳转,一共需要更新path和匹配path对应组件这两步。
精准匹配很好理解,就是path的路径和路由的路径必须完全相同才能匹配成功。
而模糊匹配,则是只要path能够覆盖route的路径,不需要完全匹配即可匹配成功。当 Link标签中定义的path为二级(或以上级别)的路由,那么只需要其第一级的path和Route定义的path一致即可匹配成功;反之,如果是Link表中定义的path只是一级路由,而Route不存在完全匹配的路由,则匹配失败。 举例来说,<Link to="/home/a/b"> <Route path="/home" component={Xxxx} />
在模糊匹配的条件下,是可以成立的。而 <Link to="/home"> <Route path="/home/a/b" component={Xxxx} />
这种情况下是不可以匹配成功的。
我们可以通过下面的例子来加深一下对精准匹配和模糊匹配的理解:
(一)精准匹配
常规的精准匹配要求,跳转的路径和路由中配置的路径要完全匹配。由于路由默认是模糊匹配,如果需要开启精准匹配,需要我们在Route
组件中配置exact=true
或者直接简写exact
。精准匹配使用简单,但实际上,exact 在实际应用中还是尽量要谨慎使用,避免出现二级(n级)路由匹配不到的问题出现。(详见下一节的嵌套路由)
export default class App extends Component {
render() {
return (
<div>
...
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
<Route exact path="/about" component={About}/>
<Route exact path="/home" component={Home}/>
</div>
</div>
</div>
</div>
</div>
)
}
}
(二)模糊匹配
在下面的案例中,<MyNavLink>
组件(自己对NavLink组件的封装)中配置的路径为/about/test
和/home/test
,和路由配置的path并不完全一致。(属于前者为后者的子集),此时是可以正常跳转的。
export default class App extends Component { render() {
return (
<div>
...
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<MyNavLink to="/about/test">About</MyNavLink>
<MyNavLink to="/home/test">Home</MyNavLink>
</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>
)
}
}
我们可以尝试着换种方式,改成<MyNavLink>
配置的路径是<Route>
组件配置路径的父集,看组件能够跳转成功。
export default class App extends Component { render() {
return (
<div>
...
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
<Route path="/about/test" component={About}/>
<Route path="/home/test" component={Home}/>
</div>
</div>
</div>
</div>
</div>
)
}
}
我们可以从上图中看到,此时About组件的内容是没有出来的,也就是说模糊匹配并不支持浏览器跳转的url是
<Route>
组件配置路径的父集这个情景。上述这种情况其实不难理解,我们真正去匹配一个组件时,应该是以Route配置为准,从父组件(一级路由)一步一步地匹配到子组件(n级路由)的。反之,一级路由下往往可能存在有多个子路由的可能,此时就必然会面临页面不知道选择哪个子路由作为跳转组件的问题。
五、嵌套(多级)路由
随着组件的复杂度组件上升,有时候我们可能会在组件中嵌套多级的路由,现在我们在上一小节的基础上,新增一个需求:在Home组件中中新增2个组件,并对2个组件配置对应的二级路由。
这里的话省略2个新组件的代码(要的话可以从文章最后的码云链接上面看),直接看App组件和Home组件是怎么配置路由的:
App组件
export default class App extends Component {
render() {
return (
<div>
...
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
<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}/>
</Switch>
</div>
</div>
</div>
</div>
</div>
)
}
}
Home组件:
export default class Home extends Component {
render() {
return (
<div>
<h3>我是Home的内容</h3>
<ul class="nav nav-tabs">
<li><MyNavLink to="/home/news">News</MyNavLink></li>
<li><MyNavLink to="/home/messages">Message</MyNavLink></li>
</ul>
<Route path="/home/news" component={News} />
<Route path="/home/messages" component={Message} />
</div>
)
}
}
我们可以看到,在Home组件中,我们<NavLink>组件和<Route>组件对应的路由都是二级路由,前者很好理解,<NavLink>
的配置的to属性表示接下来浏览器将要跳转的路径,单纯地写/new
的路径实际的请求结果将为:localhost:8080/new
,不会带上上一级的路由。
而Home组件的Route虽然是嵌套在App组件中的路由中,但是对应的一级路由还是必须写的。因为路由器在匹配的时候,是根据路由注册的层级/时机来逐层匹配的,也就是说,/news 会先去App.js中的路由中进行匹配,如果匹配不到,那么是到达不了这个页面的路由的,所以这时,就需要在Link标签中使用二级路由了。
所以说,如果在父路由中使用了精准匹配,那么对于子路由的请求基本上都会被拦截下来,使用不了的。
六、组件路由传递参数
在上面的讲解中,我们知道了组件如何进行跳转,但是如果我们需要组件在跳转过程中携带参数到下一个组件中,我们可以怎么实现呢?常见的组件路由传递参数的方式有三种,分别是通过params参数传递、利用search属性传递、利用state属性传递,下面我们就来看一下这三种方式的具体使用吧。
(一)通过params进行参数传递
步骤一:在<Link>组件上传递参数
一般来说,我们会使用类似于/${id}
的方式来传递值
<NavLink to={`/home/messages/details/${messageObj.id}/${messageObj.title}`}>xxx</NavLink>
步骤二:在路由上写好每个属性对应匹配的值
步骤一中虽然传递了多个参数,但是我们并不知道每个参数对应的key是什么,所以需要先规定好每个参数对应的key。
<Route path={'/home/messages/details/:id/:title'} component={Details}/>
步骤三:在目标组件中使用this.props.match
来获取传递的参数
export default class Details extends Component {
state = {
details: [
{id:'01',comment:'你好,我的祖国'}
...
]
}
render() {
const {details} = this.state;
const {params} = this.props.match;
const detailofMessage = details.find((detailObj)=>{
return detailObj.id === params.id
})
return (
<ul>
<li>ID: {detailofMessage.id}</li>
<li>Title: {params.title}</li>
<li>Comment: {detailofMessage.comment}</li>
</ul>
)
}
}
为什么通过props可以获取到参数呢?我们不妨打印一下this.props
看一下此时的Details 组件到底接收了哪些信息。
我们可以看到,此时组件的props属性中,有着三个属性,分别是history
,location
和match,而
match`属性中已经帮我们把传递的参数封装成为一个对象了。
(二)组件路由通过search属性传递参数
这种方式基本上和我们常见的在url地址上进行参数拼接是一样的,这种方式写法较为简单,只需要在 Link 标签中定义好key=value形式参数拼接,然后在实际的组件中,直接从prop属性中获取,调用querystring(或者其他处理字符串转对象的函数库)处理参数,并封装成对象返回即可
步骤一:在<Link>
组件上实现参数的拼接
<NavLink to={`/home/messages/details?id=${messageObj.id}&title=${messageObj.title}`}>xxx</NavLink>
步骤二:配置路由(其实这一步可以省略,因为这种方式下的传参并不需要额外对路由进行配置)
<Route path={'/home/messages/details'} component={Details}/>
步骤三:在实际组件中获取传递的参数
我们先来看一下此时目标组件接收到的props
参数是什么?
我们可以发现,此时
props.location.search
属性中把我们步骤一的参数都封装了进去,但是这种方式封装的参数是字符串形式的,我们并不能直接使用,而是要借助queryString
库来帮助我们把字符串解析成对象。
import React, { Component } from 'react'
// querystring 是脚手架初始化的时候自带的函数库,能帮助我们分解和生成key=value形式的对象
import qs from 'querystring'
export default class Details extends Component {
state = {
details: [
{id:'01',comment:'你好,我的祖国'}
...
]
}
render() {
console.log('this.props',this.props)
const {details} = this.state;
const {search} = this.props.location;
// parse能将字符串转为key:value形式的对象
const messageObj = qs.parse(search.slice(1));
const detailofMessage = details.find((detailObj)=>{
return detailObj.id === messageObj.id
})
return (
<ul>
<li>ID: {detailofMessage.id}</li>
<li>Title: {messageObj.title}</li>
<li>Comment: {detailofMessage.comment}</li>
</ul>
)
}
}
(三)通过state属性来进行参数的传递
这种形式的参数传递很有意思,和前两种传递参数的方式不同,这种方式传递的参数是不会在浏览器中显式的展示的,同时,即使刷新页面,路由子页面的数据还是不会消失。原因是react路由器帮我们对维护了history对象(history对象中又维护了location对象,所以也就有了state对象)
步骤一:在<Link>组件上传递参数
需要注意的是,和其他两种方式不同,这里的话to
属性并不是直接传一个字符串,而是传了一个对象,对象中包含了pathname
以及state
。
<NavLink to={{pathname:'/home/messages/details',state:{id:messageObj.id,title:messageObj.title}}}>xxx</NavLink>
步骤二:配置路由(其实这一步可以省略,因为这种方式下的传参并不需要额外对路由进行配置)
<Route path={'/home/messages/details'} component={Details}/>
步骤三:在具体组件中获取传递的参数
和之前一样,我们先在目标组件中将接受到的prpos
进行打印输出,我们可以看到,此时在location.state
属性中,我们可以获取到我们在步骤一中传递的参数。
export default class Details extends Component {
state = {
details: [
{id:'01',comment:'你好,我的祖国'}
...
]
}
render() {
const {details} = this.state;
const {id,title} = this.props.location.state;
const detailofMessage = details.find((detailObj)=>{
return detailObj.id === id
})
return (
<ul>
<li>ID: {detailofMessage.id}</li>
<li>Title: {title}</li>
<li>Comment: {detailofMessage.comment}</li>
</ul>
)
}
}
七、push和replace跳转和编程式路由
(一)push和replace跳转的区别
我们知道,url跳转方式可以有2种,一种是push(把本次操作记录推入history的栈中),还有一种是replace(把上一次操作记录替换成本次操作记录)。<Link>
组件默认使用的跳转方式是push
,如果想使用replase进行跳转,我们可以通过在组件上新增replace=true
或者是使用简写方式replace
来进行修改。
我们在上一小节的基础上,新增一个新的需求,每次跳转到Details组件时,都使用replace这种跳转方式
<NavLink replace to={{pathname:'/home/messages/details',state:{id:messageObj.id,title:messageObj.title}}}>xxx</NavLink>
这个特性比较简单,这里就不使用图来表示了...
(二)编程式路由
除了使用路由组件进行跳转之外,其实我们自己也可以利用事件处理函数来走路由跳转和参数传递的功能。
go & goBack & goForward
在路由组件中,我们可以通过this.prop.history
获取到history对象,然后使用对象对应的API实现(历史)路径的跳转。简单理解的话,就是通过路由组件的history对象,实现页面前进、后退的功能。
export default class Message extends Component {
state = {...}
// 前进+1
forward = ()=>{
this.props.history.goForward();
}
// 后退+1
back = ()=>{
this.props.history.goBack();
}
// 后退+2
go = ()=>{
this.props.history.go(-2);
}
render() {
const { messages } = this.state;
return (
<div>
...
<button onClick={this.forward}>前进</button>
<button onClick={this.back}>后退</button>
<button onClick={this.go}>后退+2</button>
</div>
)
}
}
通过history对象主动调用push/replace
方法
我们可以在路由组件中通过this.props.history
获取到history对象后,通过主动调用push或者replace方法来进行路由的跳转,需要注意的是,当通过编程式事务主动进行路由跳转时,对应的参数需要和之前一样,根据传递方式的不同来定义:
export default class Message extends Component {
state = {...}
pushShow = (id,title)=>{
方式1,采用 params 参数传递
// this.props.history.push(`/home/messages/details/${id}/${title}`)
方式2, 采用 search 参数传递
// this.props.history.push(`/home/messages/details?id=${id}&title=${title}`)
方式3,采用state 参数传递 (state中的id和title采用了简写方式,完整写法应该是id:id,title:title)
this.props.history.push({pathname:'/home/messages/details',state:{id,title}})
}
replaceShow = (id,title) =>{
方式1,采用 params 参数传递
// this.props.history.replace(`/home/messages/details/${id}/${title}`)
方式2: 采用 search 参数传递
// this.props.history.replace(`/home/messages/details?id=${id}&title=${title}`)
方式3,采用state 参数传递
this.props.history.replace({pathname:'/home/messages/details',state:{id,title}})
}
...
render() {
const { messages } = this.state;
return (
<div>
<div>
<ul>
{
messages.map((messageObj) => {
return (
<li key={messageObj.id}>
{/* 传递state参数 */}
<NavLink replace to={{ pathname: '/home/messages/details', state: { id: messageObj.id, title: messageObj.title } }}>{messageObj.title}</NavLink>
{/* <NavLink replace to={`/home/messages/details/${messageObj.id}/${messageObj.title}`}>{messageObj.title}</NavLink> */}
{/* <NavLink replace to={`/home/messages/details?id=${messageObj.id}&title=${messageObj.title}`}>{messageObj.title}</NavLink> */}
<button onClick={()=>{this.pushShow(messageObj.id,messageObj.title)}}>push跳转</button>
<button onClick={()=>{this.replaceShow(messageObj.id,messageObj.title)}}>replace跳转</button>
</li>
)
})
}
</ul>
</div>
<Route path={'/home/messages/details'} component={Details} />
{/* <Route path={'/home/messages/details/:id/:title'} component={Details} /> */}
...
</div>
)
}
}
八、withRouter的使用
我们知道,在路由组件中,我们可以使用props携带的参数来实现路由的跳转以及参数的获取,那么对于非路由组件来说,如果想要路由组件的功能的话,可以怎么实现呢?
针对这个需求,react-router-dom
就提供了withRouter
这个函数,帮助我们实现把一般组件包装为路由组件。下面的Header组件原本是一个一般组件,我们通过使用withRouter
函数,把原先的Header组件作为参数传入,返回值就是一个包装后的新组建
import React, { Component } from 'react';
import {withRouter} from 'react-router-dom'
class Header extends Component {
goForward = ()=>{
this.props.history.goForward();
}
goBack = ()=>{
this.props.history.goBack();
}
go = ()=>{
this.props.history.go(2);
}
render() {
return (
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
<button onClick={this.goForward}>前进</button>
<button onClick={this.goBack}>后退</button>
<button onClick={this.go}>前进+2</button>
</div>
)
}
}
export default withRouter(Header)
说在最后
实际上,React路由中的内容还是比较多的,本篇文章只是把常用的组件和函数进行讲解而已,更多的使用方法还是要在实际项目中具体去查看文档加以使用。
文章中用到案例已经放在了码云上,有需要的可以自行下载:https://gitee.com/moutory/react-staging