查看专栏其它文章:
② React 面向组件编程(state、props、refs)、事件处理
④ React 列表与Keys、虚拟DOM相关说明、AJAX
⑥ React 项目中的AJAX请求、组件间通信的2种方式(props、消息订阅和发布)
本人是个新手,写下博客用于自我复习、自我总结。
如有错误之处,请各位大佬指出。
学习资料来源于:尚硅谷
因为 React-Router 整体内容繁杂,所以为了文章内容准确性,参考了一些文章。其链接如下:
react-router
react-router 是 React 的一个插件库,它专门用来实现一个 SPA 应用(单页应用)。基于 react 的项目基本都会用到此库。那么何为单页应用?
在一个项目中,通常承载的功能页面是很多的,这就需要我们的页面不断变化。但是页面的变化不能总依靠页面的跳转,随着功能的增加,这必然会影响效率。所以为了解决这个问题单页应用出现了。
单页应用就是 整个应用只有一个完整的页面,点击页面中的链接不会刷新页面,本身也不会向服务器发请求,而是只去做页面的局部更新。
而 React-Router 被称为路由库,何为路由?
路由:一个映射关系 ( key - value ),key 是 路由路径(path),value 是前台路由(组件 component )或后台路由(处理请求的回调函数 function)。
这其中后台路由是 node 服务器端路由,value 是 function。在这里,路由器是 router,它负责管理路由。路由就是 route 。如果要注册路由:router.get(path, function(req, res))
,此时当 node 接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据。
而前台路由是 浏览器端路由,value 是 component。此时如果要注册路由:<Route path="/about" component={About}>
。当请求的是路由 path 时,界面就会更新显示对应的组件,且浏览器端不发送 http 请求。 因此,react-router 专门用来实现 SPA 应用。
现在这么说还是很抽象,先来看一下最基础的用法。(本文不包括 CSS 代码)
基础用法
在使用前先安装:npm install --save react-router-dom
(用于 Web 端)
这个 react-router-dom 依赖于 react-router ,所以 react-router 也会被自动安装上。
本例将实现下图中的效果。
在本例中,App 依然为根组件。在根组件中,将会有两个路由链接,通过点击这两个链接,从而让右侧内容区显示不同路由组件中的内容。
项目结构可按下图创建。 普通组件放入 components ( App ),路由组件放入 views ( About、Home)。
About、Home中的内容:
import React from 'react'
export default function About() {
return <div>About组件内容</div>
}
import React from 'react'
export default function Home() {
return <div>Home组件内容</div>
}
Router 路由器组件
index.js
在 index.js 中,我们现在需要使用 路由器Router组件 来包裹。顶层的 Router 组件负责分析监听 URL 的变化,在它之下的 路由Route 组件(注意比 Router 少一个“r”)就可以直接读取这些信息。如果不这样做,路由效果就无法实现。
而 Router 又至少可分为两种:
第一种 BrowserRouter 。假设它用在本例中,我们这么做: / 对应 Home 页,/about 对应 About 页。但是这样的设计需要服务器端渲染,因为用户可能直接访问任何一个 URL,服务器端必须能对 / 的访问返回 HTML,也要对 /about的访问返回 HTML。
第二种 HashRouter 。它相比于第一种,需要通过 URL 后面的 # 部分来决定路由。比如:/#/ 对应 Home 页,/#/about 对应 About 页。在这种用法下,URL 中 # 之后的部分不会发送给服务器,所以无论哪个 URL,最后都是访问服务器的 / 路径,服务器也只需要返回同样一份 HTML 就可以,然后由浏览器端解析 # 后的部分,完成浏览器端渲染。
对于这两种,因为 create-react-app 产生的应用默认不支持服务器端渲染,所以平常写项目时,可以选择使用 HashRouter。但在实际产品中,最好还是用 BrowserRouter,这样用户体验更好。
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter} from 'react-router-dom'
import App from './components/app'
ReactDOM.render(
(
<BrowserRouter>
<App/>
</BrowserRouter>
),
document.getElementById('root')
)
Link、Switch、Route、Redirect
app.jsx
在根组件 app.jsx 中会涉及到很多内容:
(使用以下内容时,一定需要:import {Route, Switch, Redirect, Link, NavLink} from 'react-router-dom'
)
(1)Link
对于单页应用我们已经知道,接下来将会在不同“页面”之间切换。之前无论是 a 标签做的,还是 div + click事件 做的,想要实现往往需要一个“导航栏”。现在我们不想让浏览器行为是网页跳转,所以不可能用之前的方式来做。
而现在的解决方案就是:用 react-router 提供的 Link 组件。它会在用户点击时,让 react-router 知晓这是一个单页应用的链接,不用网页跳转只做局部页面更新 (相当于 a 标签)。
演示代码如下:
<div className="list-group-item"><Link to='/about'>About</Link></div>
<div className="list-group-item"><Link to='/home'>Home</Link></div>
除此以外,还可以用 NavLink 组件 来实现该功能。NavLink 是 Link 的一个特定版本,会在匹配上当前的url的时候给已经渲染的元素添加参数。组件的属性有:
activeClassName(string):设置选中样式,默认值为active
activeStyle(object):当元素被选中时,为此元素添加样式
exact(bool):为true时,只有当导致和完全匹配class和style才会应用
strict(bool):为true时,在确定为位置是否与当前URL匹配时,将考虑位置pathname后的斜线
isActive(func):判断链接是否激活的额外逻辑的功能
演示代码如下:
<NavLink className="list-group-item" to='/about' activeClassName='activeClass'>About</NavLink>
<NavLink className="list-group-item" to='/home' activeClassName='activeClass'>Home</NavLink>
在这里使用的 activeClassName 是指该链接被选中时的样式。设置好样式后不要忘记导入css文件:import './xxx.css'
.activeClass {
color: red !important;
}
Link 和 NavLink 的更多比对和用法可参考:React Router中Link和NavLink的学习总结,该有的内容文章都有写。
针对上面 NavLink 的用法,如果所有路由链接都有相同的 activeClassName,想要修改时就需要一个一个修改( 当然开发工具也有统一修改功能 )或者 有其它用法,为了更加方便管理,我们可以这样做:
<MyNavLink className="list-group-item" to='/about'>About</MyNavLink>
<MyNavLink className="list-group-item" to='/home'>Home</MyNavLink>
这里的 MyNavLink 是自定义的组件,不要忘记导入组件:import MyNavLink from './my-nav-link'
当我们对其样式或者功能要做出修改时,我们只需要去 MyNavLink 中,统一修改即可。需要注意的是:这里需要使用 props 才能知道要跳转的路由链接。( 那么props 中到底有什么内容将会在文章下方:向路由组件传递数据 说明)
import React from 'react'
import {NavLink} from 'react-router-dom'
export default function MyNavLink(props) {
return <NavLink {...props} activeClassName='activeClass'/>
}
(2)Switch + Route
设置好路由链接之后,为了根据用户点击去显示各个路由组件的内容,需要使用 Switch 和 Route。演示代码:
<div className="panel-body">
<Switch>
<Route path='/about' component={About}/>
<Route path='/home' component={Home}/>
</Switch>
</div>
(3)Redirect
当我们设置好以上路由链接后,初始情况是不会进入任何路由链接的。如果我们想默认跳转到某路由链接,只需要使用 Redirect 即可。演示代码:
<div className="panel-body">
<Switch>
<Route path='/about' component={About}/>
<Route path='/home' component={Home}/>
<Redirect to='/about'/>
</Switch>
</div>
完整代码:
import React from 'react'
import {Route, Switch, Redirect, Link, NavLink} from 'react-router-dom'
import About from '../views/about'
import Home from '../views/home'
export default class App extends React.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">
{/*导航路由链接*/}
<div className="list-group-item"><Link to='/about'>About</Link></div>
<div className="list-group-item"><Link to='/home'>Home</Link></div>
<NavLink className="list-group-item" to='/about'>About</NavLink>
<NavLink className="list-group-item" to='/home'>Home</NavLink>
</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>
)
}
}
除此以外,还可以实现 动态路由。假设,我们增加一个新的页面叫 Product,对应路径为 /product,但是只有用户登录了之后才显示。如果用静态路由,我们在渲染之前就确定这条路由规则,这样即使用户没有登录,也可以访问 product,我们还不得不在 Product 组件中做用户是否登录的检查。
如果用动态路由,则只需要在代码中的一处涉及这个逻辑:
<div className="panel-body">
<Switch>
<Route path='/about' component={About}/>
<Route path='/home' component={Home}/>
{
isUserLogin() &&
<Route path='/product' component={Product}/>,
}
</Switch>
</div>
嵌套路由
Route 路由组件 是可以嵌套的。现在就实现下图中的内容:在 Home 组件中,加入两个 子路由链接。
( 如果对相关基本语法感到困惑,可参照我的 ① ~ ④ 篇 测试语法文章)
my-nav-link 中的内容:( 之前提到的自定义NavLink组件 )
import React from 'react'
import {NavLink} from 'react-router-dom'
export default function MyNavLink(props) {
return <NavLink {...props} activeClassName='activeClass'/>
}
home.jsx
在 home 路由组件 中,添加两个子路由链接,和之前的做法是相同的。只不过因为 子路由 在 home 下,所以 path 的路径如下设置即可。
import React from 'react'
import {Switch, Route, Redirect} from 'react-router-dom'
import MyNavLink from '../components/my-nav-link'
import News from './news'
import Message from './message'
export default function Home() {
return (
<div>
<h2>Home组件内容</h2>
<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>
)
}
News.jsx
import React from 'react'
export default class News extends React.Component {
state = {
newsArr: ['news001', 'news002', 'news003']
}
render () {
return (
<div>
<ul>
{
this.state.newsArr.map((news, index) => <li key={index}>{news}</li>)
}
</ul>
</div>
)
}
}
Message.jsx
import React from 'react'
export default class Message extends React.Component {
state = {
messages: [
{id: 1, title: 'Message001'},
{id: 3, title: 'Message003'},
{id: 6, title: 'Message006'},
]
}
render () {
return (
<div>
<ul>
{
this.state.messages.map((message, index) => <li key={index}>{message}</li>)
}
</ul>
</div>
)
}
}
这些基本用法还是简单的,接下来在上面代码的基础上做其它功能:在 message 组件中,通过点击 路由链接,在下方内容区显示相关内容详情。( 按钮功能的实现 将在后续提及 )
在这里和以往遇到的问题不同的是:这次的几个路由链接中的内容是相似的,我们最好将这部分内容写在同一个组件中,否则在遇到更多相似内容的路由链接时,去为每个路由创建相关组件也不现实。所以该如何通过点击这些路由链接,让其内容详情子路由组件 message-detail 展现不同内容,就是这里需要解决的问题。
向路由组件传递数据
为了解决上述提到的问题,我们可以通过传递数据解决。简单地说:当我们点击某路由链接,那我们就将能够区别这些路由链接的标识传递出去,再在子组件中比对,得知要显示的是哪部分内容即可。React-Router 也允许我们向路由组件传递数据。
因此这部分数据可以这么设计:用 id 去作为标识。
state = {
messages: [
{id: 1, title: 'Message001'},
{id: 3, title: 'Message003'},
{id: 6, title: 'Message006'},
]
}
那么在子组件中,父组件到底传递了什么?
在这里会用到文章上方提到的一种用法。在之前 自定义 NavLink 组件时,我们就曾接收过父组件传递过来的数据,但是没有去查看。代码如下:
import React from 'react'
import {NavLink} from 'react-router-dom'
export default function MyNavLink(props) {
return <NavLink {...props} activeClassName='activeClass'/>
}
那么现在去 message 组件的 render 中,我们输出 props,看一下从父组件中接收到了什么:
render () {
console.log(this.props)
...
}
输出结果:
在这里需要注意 history 和 match。
match
首先我们看一下里面的内容:
值得注意的是 path 和 params。这里的 path 就是当前路由链接的路径,params 就是从父组件中接收到的参数。正因为 params 的存在,我们就能够接收父组件传递过来的参数 。
对于 path 的作用:在嵌套路由的代码中我们看到:<Route path='/home/message' component={Message} />
。那现在要添加的新路由是在 message 中,因此新 path 是要在之前 path 的基础上进行扩展。那如果真的路由层数很多的情况下,一直在 原path 的后面添加,肯定会觉得冗余,也有可能写错。因此,我们就可以用这个 path 来替代这部分内容。
在这里需要注意:之前我们使用 NavLink 或 Link,指向的路由链接都是用字符串表示的,这没有问题。但是现在,我们的 path 是获取到的:const path = this.props.match.path
,所以肯定不能用之前字符串的方式表示,我们需要让其注意到这里的 path 是参数名。同样地,为了展现不同的内容,路由链接也需要根据 id 变化。而对于 JSX 的语法规则,如果想要让其以 JS 的语法解析,外层需要用 {
。所以这样做:
<Link to={`${path}/${m.id}`}>{m.title}</Link>
它路由链接的结果是:/home/message/1
或 /home/message/3
或 /home/message/6
,由后面的 id 来决定。因此在这里也就作出了区分。
把 Route 加上,message.jsx 的完整代码:
import React from 'react'
import {Link, Route} from 'react-router-dom'
import MessageDetail from "./message-detail"
export default class Message extends React.Component {
state = {
messages: []
}
componentDidMount () {
// 模拟发送ajax请求
setTimeout(() => {
const data = [
{id: 1, title: 'Message001'},
{id: 3, title: 'Message003'},
{id: 6, title: 'Message006'},
]
this.setState({
messages: data
})
}, 1000)
}
render () {
console.log(this.props)
const path = this.props.match.path
return (
<div>
<ul>
{
this.state.messages.map((m, index) => {
return (
<li key={index}>
<Link to={`${path}/${m.id}`}>{m.title}</Link>
</li>
)
})
}
</ul>
<hr/>
<Route path={`${path}/:id`} component={MessageDetail}></Route>
</div>
)
}
}
Route 里的 :
就是用来匹配URL的一个部分,直到遇到下一个 /、?、#为止。
现在去子组件中,我们看一下 props 是否接收到了参数:
这就证明了可以接收到参数。因此子组件 message-detail.jsx 的完整代码:
import React from 'react'
const messageDetails = [
{id: 1, title: 'Message001', content: '我爱你, 中国'},
{id: 3, title: 'Message003', content: '我爱你, 老婆'},
{id: 6, title: 'Message006', content: '我爱你, 孩子'},
]
export default function MessageDetail(props) {
console.log(props);
const id = props.match.params.id
const md = messageDetails.find(md => md.id===id*1)
return (
<ul>
<li>ID: {md.id}</li>
<li>TITLE: {md.title}</li>
<li>CONTENT: {md.content}</li>
</ul>
)
}
history
接下来将会实现下图中按钮中的功能。
在这里,我们再去看在 message 组件输出的 props 中,想要实现这部分功能,将会用到这里的 history。
需要说的是, React Router 是建立在 history 之上的。 而这个 history 库是管理浏览器会话历史的工具库,它包装的是原生 BOM 中 window.history 和 window.location.hash 。简而言之,一个 history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location 对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。
里面的内容如下:
在这里就介绍常用的几个:
history.push():添加一个新的历史记录
history.replace():用一个新的历史记录替换当前的记录
history.goBack():回退到上一个历史记录
history.goForword():前进到下一个历史记录
history.go(n):前进 或 后退 第 n 个历史记录
history.push 这个方法会向 history 栈里面添加一条新记录,这个时候用户点击浏览器的回退按钮可以回到之前的路径。history.replace 跟 history.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样,替换掉当前的 history 记录。
对 history 感兴趣,可参考文章:react-router 中的history
使用起来比较简单,message.jsx 完整代码:
import React from 'react'
import {Link, Route} from 'react-router-dom'
import MessageDetail from "./message-detail"
export default class Message extends React.Component {
state = {
messages: []
}
componentDidMount () {
// 模拟发送ajax请求
setTimeout(() => {
const data = [
{id: 1, title: 'Message001'},
{id: 3, title: 'Message003'},
{id: 6, title: 'Message006'},
]
this.setState({
messages: data
})
}, 1000)
}
ShowDetail = (id) => {
this.props.history.push(`/home/message/${id}`)
}
ShowDetail2 = (id) => {
this.props.history.replace(`/home/message/${id}`)
}
back = () => {
this.props.history.goBack()
}
forward = () => {
this.props.history.goForward()
}
render () {
console.log(this.props)
const path = this.props.match.path
return (
<div>
<ul>
{
this.state.messages.map((m, index) => {
return (
<li key={index}>
<Link to={`${path}/${m.id}`}>{m.title}</Link>
<button onClick={() => this.ShowDetail(m.id)}>查看详情(push)</button>
<button onClick={() => this.ShowDetail2(m.id)}>查看详情(replace)</button>
</li>
)
})
}
</ul>
<p>
<button onClick={this.back}>返回</button>
<button onClick={this.forward}>前进</button>
</p>
<hr/>
<Route path={`${path}/:id`} component={MessageDetail}></Route>
</div>
)
}
}