React页面路由 react-router-dom
1 背景
随着ajax
的使用越来越广泛,前端的页面逻辑开始变得越来越复杂,特别是单页Web
应用(Single Page Web Application,SPA
)的兴起,前端路由系统随之开始流行。
所谓单页Web
应用,就是只有一张Web
页面的应用,是加载单个HTML
页面并在用户与应用程序交互时动态更新该页面的Web
应用程序。浏览器一开始会加载必须的HTML
、CSS
和Javascript
,所有的操作都在这张页面上完成,都由Javascript
来控制。
- 从用户的角度看,前端路由主要实现了两个功能(使用
ajax
更新页面状态的情况下):- 记录当前页面的状态(保存或分享当前页的
url
,再次打开该url
时,网页还是保存(分享)时的状态); - 可以使用浏览器的前进后退功能(如点击后退按钮,可以使页面回到使用
ajax
更新页面之前的状态,url
也回到之前的状态);
- 记录当前页面的状态(保存或分享当前页的
- 作为开发者,要实现这两个功能,我们需要做到:
- 改变
url
且不让浏览器向服务器发出请求; - 监测
url
的变化; - 截获
url
地址,并解析出需要的信息来匹配路由规则。
- 改变
我们路由常用的hash
模式和history
模式实际上就是实现了上面的功能。
2 hash模式
hash
模式是一种把前端路由的路径用井号#
拼接在真实url
后面的模式。当井号#
后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发onhashchange
事件。
这里的hash
就是指 url 尾巴后的#号以及后面的字符。这里的#
和css
里的#
是一个意思。hash也称作锚点,本身是用来做页面定位的,它可以使对应id
的元素显示在可视区域内。
由于hash
值变化不会导致浏览器向服务器发出请求,而且hash
改变会触发hashchange
事件,浏览器的进后退也能对其进行控制,所以人们在html5
的history
出现前,基本都是使用hash
来实现前端路由的。
缺点:传递参数的方式是在url
后拼接,会有体积的限制
使用到的api:
window.location.hash = 'qq' // 设置 url 的 hash,会在当前url后加上 '#qq'
var hash = window.location.hash // '#qq'
window.addEventListener('hashchange', function () {
// 监听hash变化,点击浏览器的前进后退会触发
})
3 history模式
history API
是H5
提供的新特性,允许开发者直接更改前端路由,即更新浏览器URL
地址而不重新发起请求,它通过监听window
的popstate
来实现的。
history
路由特点:
- 新的
url
可以是与当前url
同源的任意url
,也可以是与当前url
一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中。 - 通过
history.state
,添加任意类型的数据到记录中。 - 可以额外设置
title
属性,以便后续使用。 - 通过
pushState
、replaceState
来实现无刷新跳转的功能。 - 可以传递复杂的参数
- 可以监听浏览器的前进、后退事件(
back
、forward
、go
)
对于history
来说,确实解决了不少hash
存在的问题,但是也带来了新的问题。具体如下:
- 使用
history
模式时,在对当前的页面进行刷新时,此时浏览器会重新发起请求。如果nginx
没有匹配得到当前的url
,就会出现404的页面。 - 而对于
hash
模式来说, 它虽然看着是改变了url
,但不会被包括在http
请求中。所以,它算是被用来指导浏览器的动作,并不影响服务器端。因此,改变hash
并没有真正地改变url
,所以页面路径还是之前的路径,nginx
也就不会拦截。 - 因此,在使用
history
模式时,需要通过服务端来允许地址可访问,如果没有设置,就很容易导致出现404的局面。
history
模式与hash
模式的比较:
1、hash
本来是拿来做页面定位的,如果拿来做路由的话,原来的锚点功能就不能用了。
2、hash
的传参是基于url
的,如果要传递复杂的数据,会有体积的限制,而history
模式不仅可以在url
里放参数,还可以将数据存放在一个特定的对象中。
相关API
如下表:
API | 说明 |
---|---|
window.history.pushState(state, title, url) | pushState 主要用于往历史记录堆栈顶部添加一条记录。state :需要保存的数据,这个数据在触发popstate 事件时作为参数传递过去title :页面标题,基本没用,一般传null ,当前所有浏览器都会忽略此参数url :设定新的历史记录的url ,缺少时表示为当前页地址。新的url 与当前url 的origin 必须是一样的,否则会抛出错误。url 可以是绝对路径,也可以是相对路径。 |
window.history.replaceState(state, title, url) | 更改当前的历史记录,与pushState 参数相同 |
window.history.state | 用于存储以上方法的data 数据,不同的浏览器读写权限不一样 |
window.onpopstate | 监听浏览器前进后退事件,pushState 与replaceState 方法不会触发 |
window.history.back() | 后退 |
window.history.forward() | 前进 |
window.history.go(1) | 前进一步,-2为后退两步 |
window.history.length | 查看当前历史堆栈中页面的数量 |
示例:当前url是 https://www.baidu.com/a/
,
①执行history.pushState(null, null, './qq/')
,则变成 https://www.baidu.com/a/qq/
;
②执行history.pushState(null, null, '/qq/')
,则变成 https://www.baidu.com/qq/
3 React路由使用:react-router-dom
3.1 安装
npm install react-router-dom
yarn add react-router-dom
以上方式默认安装最新版本,react-router-dom
的两个版本,5和6的使用方式不同,根据需要安装。
3.2 HashRouter和BrowserRouter:路由的容器
HashRouter
和BrowserRouter
决定了路由模式分别是hash
模式和history
模式,并且这两个组件是路由的容器,必须放在最外层。
1、hash模式
ReactDOM.render(
<HashRouter>
<Route path="/" component={Home}/>
</HashRouter>
)
2、history模式
ReactDOM.render(
<BrowserRouter>
<Route path="/" component={Home}/>
</BrowserRouter>
)
3.3 Route:路由与组件之间的映射
Route
实现了路径和显示组件之间的映射。
V5写法:
<Route path="/路径" component={组件} render={返回dom} location="route对象" exact="匹配规则"/>
它的参数说明如下:
参数 | 说明 |
---|---|
path | 跳转的路径 |
component | 对应路径显示的组件 |
render | 可以自己写render函数返回具体的dom,而不需要去设置component |
location | 传递route对象,和当前的route对象对比,如果匹配则跳转 |
exact | 匹配规则,默认值为false,true的时候则精确匹配。 |
V6写法:
<Route path="/路径" element={组件} render={返回dom} location="route对象"/>
示例:
<Routes>
<Route path={"/"} element={<home/>} />
<Route path={"/about"} element={<About/>} />
<Route path={"/topics"} element={<Topics/>} />
</Routes>
注意:V6中不需要exact
属性,它默认就是匹配完整路径。
3.4 Router:管理路由的状态
Router
组件是底层路由,可以管理路由的状态,可以代替HashRouter
和BrowserRouter
。
下面与<BrowserRouter></BrowserRouter>
作用相同:
<Router history={history}>
// ...
</Router>
下面与<HashRouter></HashRouter>
作用相同:
<Router history={createHashHistory()}>
// ...
</Router>
3.5 Link和NavLink:跳转链接
Link
和NavLink
组件类似于<a>
标签.
1、Link
组件的api
:
(1)to
:目标页面的路径,两种写法,表示跳转到哪个路由
// 字符串写法
<Link to="/a" />
// 对象写法
<Link to={{
pathname: '/courses',
search: '?sort=name',
hash: '#the-hash',
state: {fromDashboard: true}
}}/>
(2)replace
:就是将push
改成replace
(3)innerRef
:访问Link
标签的dom
2、NavLink
组件的api
属性:包含了Link
组件的所有api
,在Link
组件的基础上进行了扩展
(1)Link
的所有api
(2)activeClassName
:路由激活的时候设置的类名
(3)activeStyle
:路由激活设置的样式
(4)exact
:参考Route,符合这个条件才会激活active
类
(5)strict
:参考Route,符合这个条件才会激活active
类
(6)isActive
:接收一个回调函数,active
状态变化的时候回触发,返回false
则中断跳转
示例代码:
const oddEvent = (match, location) => {
if (!match) {
return false
}
console.log(match.id)
return true
}
<NavLink isActive={oddEvent} to="/a/123">组件一</NavLink>
(7)location
:接收一个location
对象,当url
满足这个对象的条件才会跳转
<NavLink to="/a/123" location={{ key:"mb5wu3", pathname:"/a/123" }}/>
3.6 Redirect与Navigate:页面重定向
Redirect
组件是V5中的页面重定向,属性和Link
相同,用法如下:
// 基本的重定向
<Redirect to="/somewhere/else"/>
// 对象形式
<Redirect
to={{
pathname: "/login",
search: "?utm=your+face",
state: {referrer: currentLocation}
}}
/>
// 采用push生成新的记录
<Redirect push to="/somewhere/else"/>
// 配合Switch组件使用,form表示重定向之前的路径,如果匹配则重定向,不匹配则不重定向
<Switch>
<Redirect from='/old-path' to='/new-path' />
<Route path='/new-path' component={Place} />
</Switch>
注:
①页面重定向:客户端向服务器端发送了两次请求
②请求转发:客户端向服务器发送了一次请求
Navigate
是V6中的页面重定向,使用方式如下:
<Route path="/" element={<Navigate to="/home" />}></Route>
3.7 Switch和Routes:路由切换
Switch
组件是V5中进行路由切换的方式,类似Tab
标签。Switch
内部只能包含Route
、Redirect
、Router
,示例代码如下:
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/:user" component={User} />
<Route component={NoMatch} />
</Switch>
Routes
组件是V6中进行路由切换的方式,示例代码如下:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
3.8 withRouter:包装器
withRouter
组件:包装器,将普通的组件包装成路由组件。包装后普通组件就可以访问路由信息(如:history
、location
、match
)
import { withRouter } from 'react-router-dom'
const MyComponent = (props) => {
const { match, location, history } = this.props
return (
<div>{props.location.pathname}</div>
)
}
const FirstTest = withRouter(MyComponent);
3.9 Router Hooks:在函数组件中获取路由信息
在Router5.x中新增加了Router Hooks用于在函数组件中获取路由信息。使用规则和React的其他Hooks一致。
(1)useHistory:返回history对象
(2)useLocation:返回location对象
(3)useRouteMatch:返回match对象
(4)useParams:返回match对象中的params,也就是path传递的参数
import React from "react";
import {useHistory} from "react - router - dom";
function backBtn(props) {
let history = useHistory;
return <button onClick={() => {
history.goBack();
}}>返回上一页</button>
}
3.10 history对象
在每个路由组件中我们可以使用this.props.history
获取到history
对象,也可以使用withRouter
包裹组件获取,在history
中封装了push
,replace
,go
等方法,具体内容如下:
History {
length: number;
action: Action;
location: Location;
push(path: Path, state?: LocationState): void; // 调用push前进到一个地址,可以接受一个state对象,就是自定义的路由数据
push(location: LocationDescriptorObject): void; // 接受一个location的描述对象
replace(path: Path, state?: LocationState): void; // 用页面替换当前的路径,不可再goBack
replace(location: LocationDescriptorObject): void; // 同上
go(n: number): void; // 往前走多少也页面
goBack(): void; // 返回一个页面
goForward(): void; // 前进一个页面
block(prompt?: boolean | string | TransitionPromptHook): UnregisterCallback;
listen(listener: LocationListener): UnregisterCallback;
createHref(location: LocationDescriptorObject): Href;
}
3.11 404视图:请求资源不存在
404视图:404错误表示客户端请求的资源不存在。在react中请求的路径不存在(404),路由采用Switch组件进行切换
<Switch>
<Route exact={true} path={"/"} component={Home}/>
<Route path={"/about"} component={About}/>
<Route path={"/topics"} component={Topics}/>
<Route component={View404}/>
</Switch>
4 react-router-dom实现路由案例
4.1 【案例1】实现简单的路由跳转
4.1.1 效果展示
启动程序,默认是Home界面,这时的地址是:localhost:3000
点击About界面,跳转到About界面,这时地址是:localhost:3000/about
点击Topics界面,跳转到Topics界面,这时地址是:localhost:3000/topics
点击话题1,页面跳转,这时地址是:localhost:3000/topics/话题1
点击话题2,地址是:localhost:3000/topics/话题2
点击话题3,地址是:localhost:3000/topics/话题3
4.1.2 实现步骤
1、用WebStorm创建一个React项目react-demo,首先安装react-router-dom模块,这里安装5.2.0版本(最新版本中的Route组件用法与本版本不同,具体在上面已经说明):
npm install react-router-dom@5.2.0
2、在src文件夹下新建文件components,用来编写不同的组件。在components文件夹下新建文件home.js,编写主页文件:
import React from "react";
class Home extends React.Component {
render() {
return (
<div>
<h2>Home页面</h2>
</div>
);
}
}
export default Home;
3、在components文件夹下新建about.js文件,代码如下:
import React from "react";
class About extends React.Component {
render() {
return (
<div>
<h2>About页面</h2>
</div>
)
}
}
export default About;
4、在components文件夹下新建topic.js文件,具体代码如下:
import React from "react";
class Topic extends React.Component {
render() {
return (
<div>
<h2>
{/*使用大括号表示这里的参数不止一条,需要从别的地方获取*/}
{/*this.props.match包含了url的信息,其中params包含了我们给定的参数*/}
{this.props.match.params.topicId}
</h2>
</div>
)
}
}
export default Topic;
5、在components文件夹下新建文件topics.js,代码如下:
import React from "react";
import {Link, Route} from "react-router-dom";
import Topic from "./topic";
class Topics extends React.Component {
render() {
return (
<div>
<h2>今日话题</h2>
<ul>
<li>
{/*Link表示要跳转到哪个页面,但并没有实现跳转,this.props.match.url获取到了当前页面的url*/}
<Link to={`${this.props.match.url}/话题1`}>
话题1:今天吃了什么
</Link>
</li>
<li>
<Link to={`${this.props.match.url}/话题2`}>
话题2:今天做核酸了吗
</Link>
</li>
<li>
<Link to={`${this.props.match.url}/话题3`}>
话题3:今天天气怎么样
</Link>
</li>
</ul>
{/*真正实现页面功能的是Route组件,exact表示默认显示当前信息*/}
{/*这里写render函数表示在页面加载出来之后首先显示该信息*/}
<Route exact path={this.props.match.url} render={() => (<h3>请选择今日话题</h3>)}/>
{/*当点击上面的话题时,跳转到topic界面,:topicId表示请求参数*/}
<Route path={`${this.props.match.url}/:topicId`} component={Topic}/>
</div>
);
}
}
export default Topics;
6、最后一步:修改App.js部分代码,将组件显示在网页上,App.js代码如下:
import {BrowserRouter, Link, Route} from "react-router-dom";
import Home from "./components/home";
import About from "./components/about";
import Topics from "./components/topics";
function App() {
return (
<BrowserRouter>
{/*编写Home、About、Topics页面的跳转*/}
<div>
<ul>
<li>
<Link to={"/"}>Home界面</Link>
</li>
<li>
<Link to={"/about"}>About界面</Link>
</li>
<li>
<Link to={"/topics"}>Topics界面</Link>
</li>
</ul>
{/*使用Route实现真正的页面跳转*/}
{/*当前默认主页面是Home界面*/}
<Route exact path={"/"} component={Home}/>
<Route path={"/about"} component={About}/>
<Route path={"/topics"} component={Topics}/>
</div>
</BrowserRouter>
)
}
export default App;
4.2 【案例2】实现计数器
4.2.1 效果展示
首页,localhost:3000:
新闻,localhost:3000/news:
点击新闻2,localhost:3000/news/1002:
课程,localhost:3000/course:
点击大数据,localhost:3000/big-data
加入我们,该页面没有编写,因此只能看到地址的改变,localhost:3000/joinUs:
4.2.2 实现步骤
1、用WebStorm创建一个React项目demo,首先安装react-router-dom模块,这里安装5.2.0版本(最新版本中的Route组件用法与本版本不同,具体在上面已经说明):
npm install react-router-dom@5.2.0
2、在src文件夹下新建文件夹components和css,components用来存放编写的组件,css用来存放样式,在components文件夹下新建文件Headers.js,编写网页的头部,代码如下:
import React, {Component} from "react";
import {NavLink} from "react-router-dom";
import "../css/header.css";
class Headers extends Component {
render() {
<header>
<nav>
<ul>
<li>
<NavLink exact to={"/"}>首页</NavLink>
</li>
<li>
<NavLink to={"/news"}>新闻</NavLink>
</li>
<li>
<NavLink to={"/course"}>课程</NavLink>
</li>
<li>
<NavLink to={"/joinUs"}>加入我们</NavLink>
</li>
</ul>
</nav>
</header>
}
}
export default Headers;
3、在css文件夹下新建header.css文件,编写css样式,代码如下:
body {
font-size: 16px;
margin: 0;
padding: 0;
}
ul {
text-align: right;
background-color: #eee;
margin: 0;
}
ul li {
display: inline-block;
list-style: none;
text-align: center;
border-left: 1px solid #ccc;
}
a {
text-decoration: none;
color: #666;
font-size: 1.5rem;
padding: 0.8em 2em;
display: block;
}
a:hover {
color: #000;
}
a:active {
background-color: #666;
color: #fff;
}
4、在src文件夹下新建文件夹pages,主要存放页面组件,在该文件夹下新建Home.js文件,用来显示主页。主页是一个计数器,代码如下:
import React, {Component} from "react";
import Headers from "../components/Headers";
import "../css/home.css";
class Home extends Component {
constructor(props) {
super(props);
// 设置状态机
this.state = {
count: 0
}
}
// 编写增加函数
add = () => {
// preState表示当前状态state
this.setState((preState) => {
return {
count: preState.count + 1
}
})
}
// 编写减函数
sub = () => {
this.setState((preState) => {
return {
count: preState.count - 1
}
})
}
// 异步函数
async asyncAdd() {
// 表示1秒后再加1
await setTimeout(() => {
this.setState((preState) => {
return {
count: preState.count + 1
}
})
}, 1000);
}
render() {
return (
<div className={"home"}>
<Headers/>
<h1>Count的值:{this.state.count}</h1>
<div className={"flexContainer"}>
<button onClick={() => this.asyncAdd()}>等待1s再执行count+1</button>
<button onClick={this.add}>count+1</button>
<button onClick={() => this.sub()}>count-1</button>
</div>
</div>
)
}
}
export default Home;
5、在css文件夹下新建文件home.css,代码如下:
@keyframes rotate {
0% {
transform: rotate(0deg);
left: 0px;
}
100% {
transform: rotate(360deg);
left: 0px;
}
}
.home {
text-align: center;
}
.logo {
animation: rotate 10s linear 0s infinite;
}
button {
background: #237889;
font-size: calc(1.5 * 1rem);
color: #fff;
padding: 0.3rem 1rem;
border-radius: 1em;
margin: 1em;
}
6、在pages文件夹下新建文件NewDetails.js,编写新闻详情页,代码如下:
import React, {Component} from "react";
import Headers from "../components/Headers";
class NewDetails extends Component {
constructor(props) {
super(props);
// props.location.state存放了新闻的具体的数据id、content以及title
this.data = props.location.state ? props.location.state.data : null;
}
render() {
if (this.data != null) {
let title = this.data.title;
let content = this.data.content;
return (
<div>
<Headers/>
<h1>{title}</h1>
<p>{content}</p>
</div>
)
}
}
}
export default NewDetails;
7、在pages文件夹下新建News.js,编写新闻主页,代码如下:
import React, {Component} from "react";
import {Route, NavLink} from "react-router-dom";
import Headers from "../components/Headers";
import NewDetails from "./NewDetails";
const data = [ // 定义新闻的内容,在实际开发中这些数据来源于后台
{
id: 1001,
title: "新闻1",
content: "北京"
}, {
id: 1002,
title: "新闻2",
content: "上海"
}
]
class NewsPage extends Component {
render() {
return (
<div>
<Headers/>
<h1>请选择一条新闻</h1>
{
data.map((item) => {
return (
<div key={item.id}>
<NavLink to={{
pathname: `${this.props.match.url}/${item.id}`,
state: {data: item}
}}>
{item.title}
</NavLink>
</div>
)
})
}
</div>
)
}
}
// Route组件实现真正的跳转
const News = ({match}) => {
return (
<div>
<Route exact path={match.path} render={(props) => <NewsPage {...props}/>}/>
<Route path={`${match.path}/:id`} component={NewDetails}/>
</div>
)
}
export default News;
8、在pages文件夹下新建文件Course.js,代码如下:
import React, {Component} from "react";
import Headers from "../components/Headers";
import {NavLink} from "react-router-dom";
class Course extends Component {
render() {
let {match} = this.props; // this.props.match存放了path和url
return (
<div>
<Headers/>
{/*浏览器的路径改变了,但是没有发生页面跳转*/}
<p>
<NavLink to={`${match.url}/front-end`}>前端技术</NavLink>
</p>
<p>
<NavLink to={`${match.url}/big-data`}>大数据</NavLink>
</p>
<p>
<NavLink to={`${match.url}/algorithm`}>算法</NavLink>
</p>
</div>
);
}
}
export default Course;
9、最后一步,在App.js文件中修改部分代码,具体代码如下:
import {BrowserRouter as Router, Route, Switch} from "react-router-dom";
import Home from "./pages/Home";
import Course from "./pages/Course";
import News from "./pages/News";
function App() {
return (
<Router>
<Switch>
<Route exact path={"/"} component={Home}/>
<Route path={"/course"} component={Course}/>
<Route path={"/news"} component={News}/>
</Switch>
</Router>
);
}
export default App;