React 路由简介
在
Web
应用中,路由系统是不可或缺的一部分,尤其是单页面应用,在浏览器URL
发生变化时,路由系统会做出一些响应,来控制组件的加载与切换,React
全家桶中也有配套的路由系统,在路由2.0
版本时叫做react-router
,在路由4.0
时更名为react-router-dom
,我们本次就针对较新版本的Router
系统进行介绍。
创建项目
为了方便演示如何 Router
,我们使用 create-react-app
创建一个 React
项目,并删除 src
文件夹内多余文件,创建我们需要的文件 index.js
,目录结构如下:
react-router
|- public
| |- favicon.ico
| |- index.html
| |- manifest.json
|- src
| |- pages
| | |- Add.js
| | |- Detail.js
| | |- Home.js
| | |- Index.js
| | |- List.js
| | |- Login.js
| | |- Logo.js
| | |- MenuLink.js
| | |- Profile.js
| | |- Protected.js
| | |- User.js
| |- App.js
| |- index.css
| |- index.js
|- .gitignore
|- package.json
|- README.md
|- yarn.lock
其中主组件为 App
,在 index.js
中渲染,index.js
代码如下:
/* 路径:~react-router/src/index.js */
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, window.root);
而 App
组件主要用来渲染菜单导航和路由组件,我们将在下面完善代码。
HashRouter 和 BrowserRouter
在 React Router
中,给我们提供了一些路由相关的组件,其中最重要的就是实现路由的 HashRouter
和 BrowserRouter
,我们知道浏览器的 hash
值发生变化会阻止页面的跳转,而 HashRouter
就是利用这个特性实现的,通过监听 onhanshchange
事件在 hash
值改变的时候做出响应,BrowserRouter
则是利用 H5
的新 History API
的 pushState
方法构造的的历史记录集合来实现的。
通常情况下,在开发的时候使用 HashRouter
更多,而在真正上线时替换成 BrowserRouter
,两种 Router
在地址栏上的表现上区别只是是否含有 #
,两种 Router
的使用如下:
/* 路径:~react-router/src/App.js —— HashRouter */
import React, { Component } from 'react';
import { HashRouter } from 'react-router-dom';
export default class App extends Component {
render() {
return (
<HashRouter>
{/* 路由相关代码 */}
</HashRouter>
)
}
}
/* 路径:~react-router/src/App.js —— BrowserRouter */
import React, { Component } from 'react';
import { BrowserRouter } from 'react-router-dom';
export default class App extends Component {
render() {
return (
<BrowserRouter>
{/* 路由相关代码 */}
</BrowserRouter>
)
}
}
其实就是使用 React Router
中提供的这两种类型的路由组件对路由相关的 JSX
进行包裹。
Route 和 Link 组件
Route
组件是用来定义路由跳转切换组件的区域,通过 path
属性定义匹配的路由,component
属性来定义渲染的组件,渲染后就是一个 div
标签,Link
是用来点击跳转路由的,通常用来定义导航栏内容,通过 to
属性设置匹配的路由,需要与 Route
的 path
一一对应,点击后可切换到对应的路由组件,渲染后为一个 a
标签。
创建路由跳转的组件
下面我们来创建三个路由对应的组件,分别为首页、用户、个人中心,对应的组件分别为 Home.js
、User.js
、Profile.js
/* 路径:~react-router/src/pages/Home.js */
import React, { Component } from 'react';
export default class Home extends Component {
render() {
return (
<div>主页</div>
)
}
}
/* 路径:~react-router/src/pages/User.js */
import React, { Component } from 'react';
export default class User extends Component {
render() {
return (
<div>用户</div>
)
}
}
/* 路径:~react-router/src/pages/Profile.js */
import React, { Component } from 'react';
export default class Profile extends Component {
render() {
return (
<div>个人中心</div>
)
}
}
配合使用 Route 和 Link
使用 Link
和 Route
配合使用如下,点击 Link
会在类名 container
的元素种加载路由路径对应的组件。
/* 路径:~react-router/src/App.js */
import React, { Component } from 'react';
import { HashRouter, Route, Link } from 'react-router-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
export default class App extends Component {
render() {
return (
<HashRouter>
<div>
<ul className="nav">
<li>
<Link to="/home">首页</Link>
</li>
<li>
<Link to="/user">用户</Link>
</li>
<li>
<Link to="/profile">个人中心</Link>
</li>
</ul>
<div className="container">
<Route path="/home" component={Home} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
</div>
</div>
</HashRouter>
)
}
}
启动项目后上面的代码已经可以帮助我们实现页面路由的切换,但是上面的代码 Link
和 Route
组件混在一起,我们其实可以将 App
拆分成两个组件,一个用来存放 Link
部分,一个用来存放 Route
部分,创建 Index
组件,将 Link
的部分抽取出去,代码修改如下:
/* 路径:~react-router/src/App.js —— 修改后 */
import React, { Component } from 'react';
import { HashRouter, Route } from 'react-router-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
import Index from './pages/Index';
export default class App extends Component {
render() {
return (
<HashRouter>
<Index>
<Route path="/home" component={Home} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
</Index>
</HashRouter>
)
}
}
/* 路径:~react-router/src/pages/Index.js */
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export default class Index extends Component {
render() {
return (
<div>
<a className="navbar-brand">管理系统</a>
</div>
<ul className="nav">
<li>
<Link to="/home">首页</Link>
</li>
<li>
<Link to="/user">用户</Link>
</li>
<li>
<Link to="/profile">个人中心</Link>
</li>
</ul>
<div className="container">
{this.props.children}
</div>
)
}
}
经过修改之后 Index
组件专门用来维护导航组件 Link
,App
组件专门用来维护路由组件 Route
,这样代码看起来就不那么混乱了。
Route 组件的 exact 属性
上面我们所定义的路由为一级路由,在路由匹配并成功加载对应组件后,如果组件又由多个组件组成,并有类似导航的操作(当然不仅限于导航)来控制其他的组件视图的切换,则需要匹配二级路由,这就出现了一个问题,我们以 /user
为例,假设添加一个新的路由 /user/add
,那么 React
会由上到下依次匹配,/user/add
中包含 /user
,因此会同时渲染两个组件,这不是我们希望的。
在 React Router
内部给我们提供了解决方案,就是给路由设置严格匹配,我们只需要让 /user
对应的 Route
组件添加 exact
属性,并将值设置为 true
即可,所以匹配 /user/add
时就不会出现 /user
对应的路由组件也被渲染的情况,当然也可以将 exact
简写到 Route
组件上省略赋值为 true
的过程。
/* 路径:~react-router/src/App.js —— 添加 exact */
import React, { Component } from 'react';
import { HashRouter, Route } from 'react-router-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
import Index from './pages/Index';
export default class App extends Component {
render() {
return (
<HashRouter>
<Index>
<Route path="/home" component={Home} />
<Route path="/user" exact component={User} />
<Route path="/user/add" component={User} />
<Route path="/profile" component={Profile} />
</Index>
</HashRouter>
)
}
}
Switch 组件
因为 React
的路由是由上至下依次进行匹配的,如果有两个同名路由进行匹配,会同时加载两个组件,这也是我们需要优化的,React Router
的 Switch
组件就是来做这件事的,只需要将多个 Route
组件包裹起来,就可以实现只要成功匹配一个路由就不再继续匹配。
/* 路径:~react-router/src/App.js —— 添加 Switch 组件 */
import React, { Component } from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
import Index from './pages/Index';
export default class App extends Component {
render() {
return (
<HashRouter>
<Index>
<Switch>
<Route path="/home" component={Home} />
<Route path="/user" exact component={User} />
<Route path="/user/add" component={User} />
<Route path="/user/add" component={User} /> {/* 同名路由 */}
<Route path="/profile" component={Profile} />
</Switch>
</Index>
</HashRouter>
)
}
}
使用 Switch
组件优化后,启动项目就可以发现只渲染了一个 User
组件。
Redirect 组件
在 React
开发中经常遇到路径输入错误的情况,通常情况有两种处理方式,第一种是跳转到一个 404
页面,第二种方式是将页面路由重定向到主页,而 React Router
提供的 Redirect
组件就是帮助我们在所有路由都匹配失败时重定向的,使用时通常放在最后一个 Route
组件的下面用来 “兜底”,使用 to
属性来定义重定向的路由。
/* 路径:~react-router/src/App.js —— 添加 Redirect 组件 */
import React, { Component } from 'react';
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
import Index from './pages/Index';
export default class App extends Component {
render() {
return (
<HashRouter>
<Index>
<Switch>
<Route path="/home" component={Home} />
<Route path="/user" exact={true} component={User} />
<Route path="/profile" component={Profile} />
<Redirect to="/home" /> {/* 无法匹配路由时重定向 */}
</Switch>
</Index>
</HashRouter>
)
}
}
注意:
Redirect
不能放在Route
组件的上面,因为放在上面不会匹配任何的路由,而会直接重定向到设置的页面。
对于路由都没有匹配而返回 404
页面我们这里也简单说一下,但是这样的用法非常少,使用 Redirect
重定向到指定页面的方式会更多一些。
/* 路径:~react-router/src/App.js —— 匹配失败跳转 404 页面 */
import React, { Component } from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
import Index from './pages/Index';
export default class App extends Component {
render() {
return (
<HashRouter>
<Index>
<Switch>
<Route path="/home" component={Home} />
<Route path="/user" exact={true} component={User} />
<Route path="/profile" component={Profile} />
<Route path="/" component={Error} /> {/* Error 组件代表 404 */}
</Switch>
</Index>
</HashRouter>
)
}
}
由于其他的路由都匹配失败,最后会和 /
匹配,所以会显示 Error
组件,这里处理 404
的 Route
的组件也必须放在最下面来 “兜底”。
二级路由
实现二级路由
在了解 React Router
的基本使用后,我们用同样的知识点来给 User
组件写一个二级路由,User
中有一个子导航,分别对应用户列表 List
组件和添加用户 Add
组件,代码的套路与之前相同。
/* 路径:~react-router/src/pages/User.js */
import React, { Component } from 'react';
import { Link, Route, Switch } from 'react-router-dom';
import Add from './Add';
import List from './List';
export default class User extends Component {
render() {
return (
<div>
<ul className="sub-nav">
<li>
<Link to="/user/list">用户列表</Link>
</li>
<li>
<Link to="/user/add">添加用户</Link>
</li>
</ul>
<div className="sub-container">
<Switch>
<Route path="/user/list" component={List} />
<Route path="/user/add" component={Add} />
</Switch>
</div>
</div>
)
}
}
解决默认路径不匹配的问题
当通过 React Router
访问 /user
的时候,会先加载 User
组件,再加载 User
内部的组件包括子导航,但是 /user
的路径既没有和 /user/add
匹配,也没有和 /user/list
匹配,这样渲染了一个空的类名为 sub-containe
的 div
标签,我们应该让 User
组件加载时子路由默认可以匹配一个路由组件,解决方式如下:
/* 路径:~react-router/src/pages/User.js —— Redirect 组件重定向的方式 */
import React, { Component } from 'react';
import { Link, Route, Switch, Redirect } from 'react-router-dom';
import Add from './Add';
import List from './List';
export default class User extends Component {
render() {
return (
<div>
<ul className="sub-nav">
<li>
<Link to="/user/list">用户列表</Link>
</li>
<li>
<Link to="/user/add">添加用户</Link>
</li>
</ul>
<div className="sub-container">
<Switch>
<Route path="/user/list" component={List} />
<Route path="/user/add" component={Add} />
<Redirect to="/user/list" /> {/* 重定向到 List 组件 */}
</Switch>
</div>
</div>
)
}
}
上面的方式是使用 Redirect
组件重定向的方式实现的,但是这样访问的 /user
,路径会自动改变为 /user/list
,感觉上有一些奇怪,当然还有另外的解决方式。
/* 路径:~react-router/src/pages/User.js —— Route 组件严格匹配 */
import React, { Component } from 'react';
import { Link, Route, Switch } from 'react-router-dom';
import Add from './Add';
import List from './List';
export default class User extends Component {
render() {
return (
<div>
<ul className="sub-nav">
<li>
<Link to="/user/list">用户列表</Link>
</li>
<li>
<Link to="/user/add">添加用户</Link>
</li>
</ul>
<div className="sub-container">
<Switch>
<Route path="/user" exact component={List} />
<Route path="/user/list" component={List} />
<Route path="/user/add" component={Add} />
</Switch>
</div>
</div>
)
}
}
上面的方式是当匹配到了 /user
的路由也加载默认要渲染的 List
组件实现的,但是为了防止向下继续匹配,可以添加 exact
设置严格匹配,进一步优化可以使用 Switch
组件,让路由成功匹配一次后不再向下匹配。
编程式导航
我们经常会遇到一个场景,就是在某些交互之后实现页面的自动跳转,而对于 React
搭建的单页面应用来说就是路由切换,在 React
中都最初是通过 Link
组件的点击手动实现的路由切换,那么怎么通过纯编程的方式在某些交互后自动切换路由呢,其实 React Router
的 Route
组件会给内部渲染的组件传递路由相关的三个参数 history
、location
和 match
。
history
上存储了length
属性代表当前支持存入历史记录的数量,也同样存储了location
,用来存储路由路径的相关信息,还有用来操作路由跳转的方法go
(传入数字代表前进或后退几页)、goBack
(后退)、goForward
(前进)、replace
(用其他路由替换当前历史)、push
,其中最常用的就是push
方法,下面会着重介绍,match
中存储了一些路由匹配的相关信息,如url
,即浏览器输入的路径,真正匹配的路径path
属性以及是代表否严格匹配的isExact
属性,在match
中最重要的是params
属性,值为对象,用来存储路由参数,这个我们放在后面来说。
下面在 Add
组件中添加一输入框和按钮,当点击按钮时将输入框的数据存入 localStorage
中,并自动将路由跳转到 /user/list
,即渲染 List
组件,然后将数据取出渲染到 List
组件中,这是一个很常见的需求,添加数据然后跳到详情页的场景,下面是 Add
组件中的实现。
/* 路径:~react-router/src/pages/Add.js */
import React, { Component } from 'react';
export default class Add extends Component {
input = React.createRef() // 非受控组件取值
// 表单提交事件
handleSubmit = (e) => {
e.preventDefault(); // 取消默认的页面跳转事件
// 先从 localStorage 获取已有数据
const lists = JSON.parse(localStorage.getItem('lists')) || [];
// 添加新数据
lists.push({
id: lists.length + 1,
username: this.input.current.value
});
// 存入 localStorage
localStorage.setItem('lists', JSON.stringify(lists));
// 编程式导航,自动跳转到 List
this.props.history.push('/user/list');
}
render() {
return (
<div>
<form className="form" onSubmit={this.handleSubmit}>
<label htmlFor="username" className="control-label">用户名</label>
<input
className="form-control"
type="text"
id="username"
ref={this.input}
/>
<br />
<input type="submit" className="btn btn-success" />
</form>
</div>
)
}
}
在上面我们通过 Route
传递给渲染组件的 history
的 push
方法实现了路由的自动跳转,push
方法接收的参数就是将要跳转的路径字符串,List
组件代码如下:
/* 路径:~react-router/src/pages/List.js */
import React, { Component } from 'react';
export default class List extends Component {
state = { users: [] }
componentWillMount() {
// 取出 localStorage 数据并更新状态
const users = JSON.parse(localStorage.getItem('lists')) || [];
this.setState({ users });
}
render() {
return (
<table>
<thead>
<tr>
<th>用户 ID</th>
<th>用户名</th>
</tr>
</thead>
<tbody>
{
this.state.users.map(({ id, username }) => {
return (
<tr key={id}>
<td>{id}</td>
<td>{username}</td>
</tr>
)
})
}
</tbody>
</table>
)
}
}
取出 localStorage
中的数据在 List
中渲染时有两点注意,第一是取出数据和设置状态应该在 render
渲染 JSX
之前,这样在没有执行 render
时会合并状态并只渲染一次,也就是说 componentWillMount
“钩子” 和 render
“钩子” 的 return
语句前更新状态都是可以的,如果在 componentDidMount
“钩子” 中更新会导致组件渲染两次,在 React
开发中如果获取数据的过程是同步的(localStorage
取值是同步的),不需要渲染两次。
第二点是在使用表格元素 table
渲染时,必须要含有 thead
和 tbody
,这是 React
规定的,不可以省略。
路由参数的传递
现在在我们的 List
组件表格中,点击每一行都可以跳转到学生 ID
对应的详情 Detail
组件中,由于每一个学生的 ID
不同渲染的详情也不相同,此时需要将学生 ID
作为路由参数进行传递,并在 Detail
内渲染对应的内容,由于 Detail
组件的渲染与 List
组件是同一区域,所以仍然是二级路由,我们需要在 User
组件中进行添加。
/* 路径:~react-router/src/pages/User.js —— 增加 Detail 二级路由 */
import React, { Component } from 'react';
import { Link, Route, Switch } from 'react-router-dom';
import Add from './Add';
import List from './List';
import Detail from './Detail';
export default class User extends Component {
render() {
return (
<div>
<ul className="sub-nav">
<li>
<Link to="/user/list">用户列表</Link>
</li>
<li>
<Link to="/user/add">添加用户</Link>
</li>
</ul>
<div className="sub-container">
<Switch>
<Route path="/user" exact component={List} />
<Route path="/user/list" component={List} />
<Route path="/user/add" component={Add} />
<Route path="/user/detail/:id" component={Detail} />
</Switch>
</div>
</div>
)
}
}
在 React Router
中,我们通过给路由后面添加 /:paramname
的方式添加参数,也可以通过 /:paramname/:paramname
传递多个参数(形参),由于在 List
中点击表格的的某行的单元格跳转路由,所以 List
组件修改如下:
/* 路径:~react-router/src/pages/List.js —— 点击跳转 Detail 并传递路由参数 */
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export default class List extends Component {
state = { users: [] }
componentWillMount() {
// 取出 localStorage 数据并更新状态
const users = JSON.parse(localStorage.getItem('lists')) || [];
this.setState({ users });
}
render() {
return (
<table>
<thead>
<tr>
<th>用户 ID</th>
<th>用户名</th>
</tr>
</thead>
<tbody>
{
this.state.users.map(({ id, username }) => {
return (
<tr key={id}>
<td>{id}</td>
<td>
<Link to={`/user/detail/${id}`}>{username}</Link>
</td>
</tr>
)
})
}
</tbody>
</table>
)
}
}
在 List
组件中,同样使用 Link
组件对要点击切换路由的节点进行包裹,并用 to
属性设置跳转的路由和路由参数(实参),现在点击就可以实现从 List
组件到 Detail
组件的切换,如果我们有些 List
的数据想在跳转到 Detail
组件时直接带过去,则可以使用另一种写法如下:
/* 路径:~react-router/src/pages/List.js —— 点击跳转 Detail 并传递路由参数和数据 */
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export default class List extends Component {
state = { users: [] }
componentWillMount() {
// 取出 localStorage 数据并更新状态
const users = JSON.parse(localStorage.getItem('lists')) || [];
this.setState({ users });
}
render() {
return (
<table>
<thead>
<tr>
<th>用户 ID</th>
<th>用户名</th>
</tr>
</thead>
<tbody>
{
this.state.users.map(({ id, username }) => {
return (
<tr key={id}>
<td>{id}</td>
<td>
<Link
to={{
pathname: `/user/detail/${id}`,
state: username
}}
>
{username}
</Link>
</td>
</tr>
)
})
}
</tbody>
</table>
)
}
}
不同的是给 to
属性传入的值从一个代表路由的字符串变成了一个对象,而把路由的字符串作为了 pathname
属性的值,state
属性则代表了路由跳转传给渲染组件的数据,还记得渲染的组件使用 Route
组件包裹的,会传入 history
、loacltion
和 match
三个属性,同样的,通过点击 Link
传递的路由参数和数据都可以在 props
上获取到,前者通过 location.state
或者 history.location.state
上获取到,后者可以通过 match.params
上获取到,那么 Detail
组件将传递过来的参数渲染,代码如下:
/* 路径:~react-router/src/pages/Detail.js */
import React, { Component } from 'react';
export default class Detail extends Component {
state = { user: {} }
componentWillMount() {
// 有值说明是点击过来的,否则是地址栏输入的
const data = this.props.location.state;
// 获取路由参数
const id = parseInt(this.props.match.params.id);
// 如果是点击过来的直接将数据设置给 state,否则去 localStorage 取值设置给 state
if (data) {
this.setState({ user: { id, username: data }});
} else {
const users = JSON.parse(localStorage.getItem('lists')) || [];
const user = users.find(item => item.id === id);
this.setState({ user: { id, username: user.username }});
}
}
render() {
return (
<div>
<span>{this.state.user.id}</span>
<span> ------- </span>
<span>{this.state.user.username}</span>
</div>
)
}
}
这里有两点注意点:
- 首先通过组件
props.match.params
获取的路由参数都是字符串格式,如果原本类型为数字,使用时应转换成数字类型;- 其次是传递的数据,也就是组件通过
props.location.state
获取的数据,只有在通过Link
组件点击过去才会存在,在地址栏输入为undefined
,所以防止用户刷新页面导致数据丢失,应该在两种情况下处理不同的获取数据的逻辑。
withRouter 函数
在之前的编程式导航中我们使用了 Route
传递给渲染组件的 props.history.push
方法实现的,现在假设我们要对一个不是路由跳转的组件,通过点击事件来获取 history
、location
和 match
属性,并使用 history
上的路由设置方法进行跳转路由,这应该如何实现呢?
其实 React Route
给我们提供了一个函数 withRouter
方法,在调用该方法时,则会返回一个新的组件,当然其实这是一个高阶组件的应用,withRouter
方法内部帮我们在传入的组件外层包装了一层 Route
组件,并传入了 history
、location
和 match
属性作为参数,所以当我们使用返回的组件时可以通过 props
属性获取 history
、location
和 match
。
下面针对我们之前的 Index
组件的内的 “管理系统” 的标签抽出一个新的组件,并将这个组件添加点击可以跳转到登录页 Login
组件的功能,Login
组件为一级路由,所以我们应该修改 App
组件,添加一个 /login
的路由,Login
和修改后的 App
组件如下:
/* 路径:~react-router/src/App.js —— 添加 Login 组件路由 */
import React, { Component } from 'react';
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
import Index from './pages/Index';
import Login from './pages/Login';
export default class App extends Component {
render() {
return (
<HashRouter>
<Index>
<Switch>
<Route path="/home" component={Home }/>
<Route path="/user" exact={true} component={User} />
<Route path="/profile" component={Profile} />
<Route path="/login" component={Login}/> {/* 添加登录页路由 */}
<Redirect to="/home" /> {/* 无法匹配路由时重定向 */}
</Switch>
</Index>
</HashRouter>
)
}
}
/* 路径:~react-router/src/pages/Login.js —— 添加登录和退出功能 */
import React, { Component } from 'react';
export default class Login extends Component {
login = () => {
localStorage.setItem('login', 'ok');
}
exit = () => {
localStorage.removeItem('login');
}
render() {
return (
<div>
<button onClick={this.login}>登录</button>
<button onClick={this.exit}>退出</button>
</div>
)
}
}
在 Login
中顺便添加了两个按钮来模拟 “登录” 和 “退出”,并给按钮添加了事件,在登录时向 localStorage
中添加 login
属性,在退出时清除这个属性,以模拟登录状态。
抽取出 Logo
后的 Index
组件也应该添加一个新的导航为 “登录”,Login
组件和修改后的 Index
组件如下:
/* 路径:~react-router/src/pages/Index.js —— 抽出 Logo 组件并添加登录导航 */
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import Logo from './Logo';
export default class Index extends Component {
render() {
return (
<div>
<Logo />
</div>
<ul className="nav">
<li>
<Link to="/home">首页</Link>
</li>
<li>
<Link to="/user">用户</Link>
</li>
<li>
<Link to="/profile">个人中心</Link>
</li>
<li>
<Link to="/login">登录</Link>
</li>
</ul>
<div className="container">
{this.props.children}
</div>
)
}
}
/* 路径:~react-router/src/pages/Logo.js */
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
class Logo extends Component {
change = () => {
console.log(this.props);
this.props.history.push('/login');
}
render() {
return (
<div className="navbar-brand" onClick={this.change}>管理系统</div>
)
}
}
export default withRouter(Logo);
通过 Logo
案例的代码我们可以看出,其实最后导出的并不是 Logo
组件,而是使用 withRouter
函数包装后返回的高阶组件,withRouter
方法内部帮我们搞定了 Logo
组件的 props
没有 history
、location
和 match
属性的问题。
受保护的路由
以前在点击个人中心时会直接渲染 Profile
组件,在给 Login
组件添加 “登录” 和 “退出” 之后,再次点击个人中心时,应该先对登录状态进行验证,如果 localStorage
中存在 login
属性,则渲染 Profile
的 Route
组件,否则重定向到登录页,如果在登录页点击登录后再重新跳回个人中心(从哪来回哪去)。
这就需要我对 App
组件路由部分的代码进行修改,使用高阶组件来添加登录验证逻辑,当然,这个高阶组件不是 React Router
提供的,需要我们自己来实现,这种做法被官方称作 “受保护的路由”。
/* 路径:~react-router/src/App.js —— 添加受保护的路由 */
import React, { Component } from 'react';
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
import Index from './pages/Index';
import Login from './pages/Login';
import Protected from './pages/Protected';
export default class App extends Component {
render() {
return (
<HashRouter>
<Index>
<Switch>
<Route path="/home" component={Home} />
<Route path="/user" exact component={User} />
{/* 添加受保护的路由 */}
<Protected path="/profile" component={Profile} />
<Route path="/login" component={Login} />
{/* 无法匹配路由时重定向 */}
<Redirect to="/home" />
</Switch>
</Index>
</HashRouter>
)
}
}
我们重写了 App
组件中个人中心对应的路由,将原来的 Route
组件用高阶组件 Protected
代替,也就等于是将原本传入的参数 path
和 component
传入了高阶组件 Protected
,下面来看一下高阶组件 Protected
的实现。
/* 路径:~react-router/src/pages/Protected.js —— 添加受保护的路由 */
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
export default class Protected extends Component {
render() {
const login = localStorage.getItem('login');
return login ? (
<Route {...this.props} />
) : (
<Redirect to={{ pathname: '/login', state: { 'from': '/profile' }}} />
)
}
}
在 Protected
获取登录状态,存在时直接渲染了 Route
组件,并将 path
和 component
参数传入,如果不存在则渲染 Redirect
组件重定向到登录页,传入的参数同 Link
组件的规则相同,pathname
代表重定向的路径,state
代表带过去的数据,我们这里添加了一个 from
属性,用来记录渲染登录页的来源,即个人中心。
接下来就是 Login
组件中在点击登录后验证是否存在 state
,如果存在则返回存储的 from
对应的路由,即个人中心,不存在则跳回首页,Login
修改如下:
/* 路径:~react-router/src/pages/Login.js —— 完善登录功能 */
import React, { Component } from 'react';
export default class Login extends Component {
login = () => {
localStorage.setItem('login', 'ok');
// 获取上一个路由传递的 state
const prevPathDate = this.props.location.state;
// 存在 state 则返回来源对应的页面,否则回主页
if (prevPathDate) {
this.props.history.push(prevPathDate.from);
} else {
this.props.history.push('/home');
}
}
exit = () => {
localStorage.removeItem('login');
}
render() {
return (
<div>
<button onClick={this.login}>登录</button>
<button onClick={this.exit}>退出</button>
</div>
)
}
}
这样 “受保护的路由” 功能就实现了,其实就是在跳转路由之前起到了一个 “拦截” 的作用,经常的使用场景是权限管理,这是一个路由的应用,也是一个高阶组件的应用,这样的应用在大型复杂的 React
中会频繁使用,还是比较重要的。
NavLink 组件
在实际项目开发中,我们经常遇到导航标签被选中时被添加一个代表 “激活” 的类名,用于添加与其他导航选项不同的样式,React Router
已经给我们提供了 NavLink
组件用于实现这个功能,NavLink
组件具备 Link
组件所有的功能,唯一不同的就是 NavLink
组件在被选中时不止发生路由跳转,还会给渲染后的 a
标签添加一个名为 active
的 class
属性,而我们只需要通过 css
去给类名 active
设置样式即可。
/* 路径:~react-router/src/index.css —— 激活样式 */
a.active {
color: skyblue !important;
}
设置好激活样式以后,我们只需要在 Index
组件中引入激活样式的 css
文件并将 Link
组件替换成 NavLink
组件即可。
/* 路径:~react-router/src/pages/Index.js —— 将 Link 修改为 NavLink */
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import Logo from './Logo';
// 引入激活样式
import '../index.css';
export default class Index extends Component {
render() {
return (
<div>
<Logo />
</div>
<ul className="nav">
<li>
<NavLink to="/home">首页</NavLink>
</li>
<li>
<NavLink to="/user">用户</NavLink>
</li>
<li>
<NavLink to="/profile">个人中心</NavLink>
</li>
<li>
<NavLink to="/login">登录</NavLink>
</li>
</ul>
<div className="container">
{this.props.children}
</div>
)
}
}
自定义导航组件实现激活
React Router
在给我们提供的导航组件 NavLink
功能有限,只会给内部的 a
标签在选中时添加 active
类名,如果我们想实现给一个 li
标签添加 active
就需要我们自己封装一个组件来实现这个功能,其实还是通过高阶组件来实现的,首先我们定义这个高阶组件的名字为 MenuLink
,将 Index
组件中的 li
标签和 NavLink
组件统一替换成 MenuLink
组件,代码如下:
/* 路径:~react-router/src/pages/Index.js —— 将 Link 修改为 NavLink */
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import Logo from './Logo';
import MenuLink from './MenuLink'
export default class Index extends Component {
render() {
return (
<div>
<Logo />
</div>
<ul className="nav">
<MenuLink to="/home">首页</MenuLink>
<MenuLink to="/user">用户</MenuLink>
<MenuLink to="/profile">个人中心</MenuLink>
<MenuLink to="/login">登录</MenuLink>
</ul>
<div className="container">
{this.props.children}
</div>
)
}
}
在实现 MenuLink
组件之前我们分析一下实现思路,首先我们依然模拟 NavLink
的方式给 MenuLink
传入了 to
属性,值为将要跳转的路由,所以我们应该在 MenuLink
组件中来接收这个路由,而 MenuLink
内部一定是包含 li
和 Link
组件的,我们可以将这个 to
属性传递给 Link
组件,如果想要通过激活状态给外层的 li
标签设置状态我们需要知道是否匹配了路由,并可以通过 match
属性获得,所以在 li
的外层应该有 Route
组件配合,因为只有 Route
组件才会将 history
、location
和 match
作为参数传递给其内部渲染的组件。
这就要说到 Route
组件的渲染模式,在传入 component
属性时,只有匹配组件才会渲染内部组件,我们显然是需要时时刻刻都渲染内部的 li
和 Link
,并通过点击 Link
渲染真正的路由组件,所以我们需要用到第二种渲染方式,就是通过 children
属性指定时刻需要渲染的组件,实现代码如下:
/* 路径:~react-router/src/pages/MenuLink.js */
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import '../index.css';
export default class MenuLink extends Component {
render() {
return (
<Route
path={this.props.to}
children={({ match }) => (
<li className={match ? 'active' : ''}>
<Link to={this.props.to}>{this.props.children}</Link>
</li>
)}
/>
)
}
}
上面代码中由于 children
组件并不需要操作状态和使用生命周期 “钩子”,所以我们直接使用了函数组件实现,因为 active
类名添加给了 li
,所以我们需要在 MenuLink
组件中引入样式文件 index.css
并将修改,代码如下:
/* 路径:~react-router/src/index.css —— 激活样式修改后 */
li.active a {
color: skyblue !important;
}
总结
本篇通过一个简单的案例使用了由
React Router
所提供的、开发中常用的功能,但美中不足的是并没有使用一些UI
库或者CSS
样式来美化,为了更明显的看到React Router
各个功能使用后的效果,建议大家在实现上面代码的同时自己添加一些CSS
样式。