# React Router 概述
React 路由
# 站点
无论是使用 Vue,还是 React,开发的单页应用程序,可能只是该站点的一部分(某一个功能块)
一个单页应用里,可能会划分为多个页面(几乎完全不同的页面效果)(组件)
如果要在单页应用中完成组件的切换,需要实现下面两个功能:
- 根据不同的页面地址,展示不同的组件(核心)
- 完成无刷新的地址切换
我们把实现了以上两个功能的插件,称之为路由
# React Router
- react-router:路由核心库,包含诸多和路由功能相关的核心代码
- react-router-dom:利用路由核心库,结合实际的页面,实现跟页面路由密切相关的功能
如果是在页面中实现路由,需要安装 react-router-dom 库
# 两种模式
路由:根据不同的页面地址,展示不同的组件
url 地址组成
例:https://www.react.com:443/news/1-2-1.html?a=1&b=2#abcdefg
- 协议名(schema):https
- 主机名(host):www.react.com
- ip 地址
- 预设值:localhost
- 域名
- 局域网中电脑名称
- 端口号(port):443
- 如果协议是 http,端口号是 80,则可以省略端口号
- 如果协议是 https,端口号是 443,则可以省略端口号
- 路径(path):/news/1-2-1.html
- 地址参数(search、query):?a=1&b=2
- 附带的数据
- 格式:属性名=属性值&属性名=属性值....
- 哈希(hash、锚点)
- 附带的数据
# Hash Router 哈希路由
根据 url 地址中的哈希值来确定显示的组件
原因:hash 的变化,不会导致页面刷新 这种模式的兼容性最好
# Borswer History Router 浏览器历史记录路由
HTML5 出现后,新增了 History Api,从此以后,浏览器拥有了改变路径而不刷新页面的方式
History 表示浏览器的历史记录,它使用栈的方式存储。
- history.length:获取栈中数据量
- history.pushState:向当前历史记录栈中加入一条新的记录
- 参数 1:附加的数据,自定义的数据,可以是任何类型
- 参数 2:页面标题,目前大部分浏览器不支持
- 参数 3:新的地址
- history.replaceState:将当前指针指向的历史记录,替换为某个记录
- 参数 1:附加的数据,自定义的数据,可以是任何类型
- 参数 2:页面标题,目前大部分浏览器不支持
- 参数 3:新的地址
根据页面的路径决定渲染哪个组件
# 路由组件
React-Router 为我们提供了两个重要组件
# Router 组件
它本身不做任何展示,仅提供路由模式配置,另外,该组件会产生一个上下文,上下文中会提供一些实用的对象和方法,供其他相关组件使用
- HashRouter:该组件,使用 hash 模式匹配
- BrowserRouter:该组件,使用 BrowserHistory 模式匹配
通常情况下,Router 组件只有一个,将该组件包裹整个页面
# Route 组件
根据不同的地址,展示不同的组件
重要属性:
- path:匹配的路径
- 默认情况下,不区分大小写,可以设置 sensitive 属性为 true,来区分大小写
- 默认情况下,只匹配初始目录,如果要精确匹配,配置 exact 属性为 true
- 如果不写 path,则会匹配任意路径
- component:匹配成功后要显示的组件
- children:
- 传递 React 元素,无论是否匹配,一定会显示 children,并且会忽略 component 属性
- 传递一个函数,该函数有多个参数,这些参数来自于上下文,该函数返回 react 元素,则一定会显示返回的元素,并且忽略 component 属性
Route 组件可以写到任意的地方,只要保证它是 Router 组件的后代元素
# Switch 组件
写到 Switch 组件中的 Route 组件,当匹配到第一个 Route 后,会立即停止匹配
由于 Switch 组件会循环所有子元素,然后让每个子元素去完成匹配,若匹配到,则渲染对应的组件,然后停止循环。因此,不能在 Switch 的子元素中使用除 Route 外的其他组件。
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
// /a
function A() {
return <h1>组件A</h1>;
}
// /a/b
function B() {
return <h1>组件B</h1>;
}
// 任意路径
function C() {
return (
<h1>
找不到页面
<Route path="/abc" exact component={D} />
</h1>
);
}
function D() {
return <span>D组件</span>;
}
export default function App() {
return (
<Router>
<Switch>
<Route path="/a/b" component={B} />
<Route path="/a" exact component={A}>
{() => {
return (
<div>
<h1 style={{ color: "red" }}>必定会看到的内容</h1>
<p>adfasdfasdf</p>
</div>
);
}}
</Route>
<Route component={C} />
</Switch>
</Router>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 路由信息
Router 组件会创建一个上下文,并且,向上下文中注入一些信息
该上下文对开发者是隐藏的,Route 组件若匹配到了地址,则会将这些上下文中的信息作为属性传入对应的组件
# history
它并不是 window.history 对象,我们利用该对象无刷新跳转地址
为什么没有直接使用 history 对象
- React-Router 中有两种模式:Hash、History,如果直接使用 window.history,只能支持一种模式
- 当使用 windows.history.pushState 方法时,没有办法收到任何通知,将导致 React 无法知晓地址发生了变化,结果导致无法重新渲染组件
- push:将某个新的地址入栈(历史记录栈)
- 参数 1:新的地址
- 参数 2:可选,附带的状态数据
- replace:将某个新的地址替换掉当前栈中的地址
- go: 与 window.history 一致
- forward: 与 window.history 一致
- back: 与 window.history 一致
# location
与 history.location 完全一致,是同一个对象,但是,与 window.location 不同
location 对象中记录了当前地址的相关信息
我们通常使用第三方库query-string,用于解析地址栏中的数据
# match
该对象中保存了,路由匹配的相关信息
- isExact:事实上,当前的路径和路由配置的路径是否是精确匹配的
- params:获取路径规则中对应的数据
实际上,在书写 Route 组件的 path 属性时,可以书写一个string pattern(字符串正则)
react-router 使用了第三方库:Path-to-RegExp,该库的作用是,将一个字符串正则转换成一个真正的正则表达式。
向某个页面传递数据的方式:
- 使用 state:在 push 页面时,加入 state
- 利用 search:把数据填写到地址栏中的?后
- 利用 hash:把数据填写到 hash 后
- params:把数据填写到路径中
# 非路由组件获取路由信息
某些组件,并没有直接放到 Route 中,而是嵌套在其他普通组件中,因此,它的 props 中没有路由信息,如果这些组件需要获取到路由信息,可以使用下面两种方式:
- 将路由信息从父组件一层一层传递到子组件
- 使用 react-router 提供的高阶组件 withRouter,包装要使用的组件,该高阶组件会返回一个新组件,新组件将向提供的组件注入路由信息。
DETAILS
import React from "react";
import { BrowserRouter as Router, Route, Switch, withRouter } from "react-router-dom";
const AWrapper = withRouter(A);
// function withRouter(Comp) {
// return function routerWrapper(props) {
// //获取上下文中的信息
// return <Comp {...props} history={上下文中的history} />
// }
// }
function News(props) {
return (
<div>
<h1>新闻列表</h1>
<AWrapper />
</div>
);
}
function A(props) {
console.log(props);
return (
<button
onClick={() => {
props.history.push("/");
}}
>
点击返回
</button>
);
}
function Index() {
return <h1>首页</h1>;
}
function NotFound() {
return <h1>找不到页面</h1>;
}
export default function App() {
return (
<Router>
<Switch>
<Route path="/news" component={News} />
<Route path="/" exact component={Index} />
<Route component={NotFound} />
</Switch>
</Router>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 其他组件
已学习:
- Router:BrowswerRouter、HashRouter
- Route
- Switch
- 高阶函数:withRouter
# Link
生成一个无刷新跳转的 a 元素
- to
- 字符串:跳转的目标地址
- 对象:
- pathname:url 路径
- search
- hash
- state:附加的状态信息
- replace:bool,表示是否是替换当前地址,默认是 false
- innerRef:可以将内部的 a 元素的 ref 附着在传递的对象或函数参数上
- 函数
- ref 对象
# NavLink
是一种特殊的 Link,Link 组件具备的功能,它都有
它具备的额外功能是:根据当前地址和链接地址,来决定该链接的样式
- activeClassName: 匹配时使用的类名
- activeStyle: 匹配时使用的内联样式
- exact: 是否精确匹配
- sensitive:匹配时是否区分大小写
- strict:是否严格匹配最后一个斜杠
# Redirect
重定向组件,当加载到该组件时,会自动跳转(无刷新)到另外一个地址
- to:跳转的地址
- 字符串
- 对象
- push: 默认为 false,表示跳转使用替换的方式,设置为 true 后,则使用 push 的方式跳转
- from:当匹配到 from 地址规则时才进行跳转
- exact: 是否精确匹配 from
- sensitive:from 匹配时是否区分大小写
- strict:from 是否严格匹配最后一个斜杠
# 导航守卫
导航守卫:当离开一个页面,进入另一个页面时,触发的事件
history 对象
- listen: 添加一个监听器,监听地址的变化,当地址发生变化时,会调用传递的函数
- 参数:函数,运行时间点:发生在即将跳转到新页面时
- 参数 1:location 对象,记录当前的地址信息
- 参数 2:action,一个字符串,表示进入该地址的方式
- POP:出栈
- 通过点击浏览器后退、前进
- 调用 history.go
- 调用 history.goBack
- 调用 history.goForward
- PUSH:入栈
- history.push
- REPLACE:替换
- history.replace
- POP:出栈
- 返回结果:函数,可以调用该函数取消监听
- 参数:函数,运行时间点:发生在即将跳转到新页面时
- block:设置一个阻塞,并同时设置阻塞消息,当页面发生跳转时,会进入阻塞,并将阻塞消息传递到路由根组件的 getUserConfirmation 方法。
- 返回一个回调函数,用于取消阻塞器
路由根组件
- getUserConfirmation
- 参数:函数
- 参数 1:阻塞消息
- 字符串消息
- 函数,函数的返回结果是一个字符串,用于表示阻塞消息
- 参数 1:location 对象
- 参数 2:action 值
- 参数 2:回调函数,调用该函数并传递 true,则表示进入到新页面,否则,不做任何操作
- 参数 1:阻塞消息
- 参数:函数
DETAILS
// app.js
import React from "react";
import { Route, Link } from "react-router-dom";
import RouteGuard from "./c";
function Page1() {
return <h1>Page1</h1>;
}
function Page2() {
return <h1>Page2</h1>;
}
export default function App() {
return (
<RouteGuard
onBeforeChange={(prev, cur, action, commit, unBlock) => {
console.log(`页面想要从${prev.pathname}跳转到${cur.pathname},跳转方式是${action},允许跳转`);
commit(true);
unBlock(); //取消阻塞,仅阻塞了一次
}}
onChange={(prevLocation, location, action, unListen) => {
console.log(`日志:从${prevLocation.pathname}进入页面${location.pathname},进入方式${action}`);
unListen(); //取消监听,仅监听了一次
}}
>
<ul>
<li>
<Link to="/page1">页面1</Link>
</li>
<li>
<Link to="/page2">页面2</Link>
</li>
</ul>
<Route path="/page1" component={Page1} />
<Route path="/page2" component={Page2} />
</RouteGuard>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// RouteGuard.js
import React, { Component } from "react";
import { BrowserRouter as Router, withRouter } from "react-router-dom";
let prevLoaction, location, action, unBlock;
class _GuardHelper extends Component {
componentDidMount() {
//添加阻塞
unBlock = this.props.history.block((newLocation, ac) => {
prevLoaction = this.props.location;
location = newLocation;
action = ac;
return "";
});
//添加一个监听器
this.unListen = this.props.history.listen((location, action) => {
if (this.props.onChange) {
const prevLoaction = this.props.location;
this.props.onChange(prevLoaction, location, action, this.unListen);
}
});
}
componentWillUnmount() {
unBlock(); //取消阻塞
//卸载监听器
this.unListen();
}
render() {
return null;
}
}
const GuardHelper = withRouter(_GuardHelper);
class RouteGuard extends Component {
handleConfirm = (msg, commit) => {
if (this.props.onBeforeChange) {
this.props.onBeforeChange(prevLoaction, location, action, commit, unBlock);
} else {
commit(true);
}
};
render() {
return (
<Router getUserConfirmation={this.handleConfirm}>
<GuardHelper onChange={this.props.onChange} />
{this.props.children}
</Router>
);
}
}
export default RouteGuard;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 应用之滚动条复位
# 高阶组件
DETAILS
import React from "react";
import { BrowserRouter as Router, Route, NavLink } from "react-router-dom";
import "./App.css";
import withScroll from "./withScroll";
function Page1() {
return <div className="page page1">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam ex at fugiat, vero ratione ullam sed dignissimos nam pa</div>;
}
function Page2() {
return <div className="page page2">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam ex at fugiat, vero ratione ullam sed dignissimos nam pariat</div>;
}
const Page1WithScroll = withScroll(Page1);
const Page2WithScroll = withScroll(Page2);
export default function App() {
return (
<Router>
<Route path="/page1" component={Page1WithScroll} />
<Route path="/page2" component={Page2WithScroll} />
<div className="nav">
<NavLink to="/page1">页面1</NavLink>
<NavLink to="/page2">页面2</NavLink>
</div>
</Router>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
withScroll.js
import React from "react";
import reset from "./resetScroll";
export default function withScroll(Comp) {
return class ScrollWrapper extends React.Component {
componentDidMount() {
reset();
}
render() {
return <Comp {...this.props} />;
}
};
}
2
3
4
5
6
7
8
9
10
11
12
13
let timer1, timer2;
/**
* 滚动条横向和纵向动画复位
*/
export default function resetScroll() {
clearInterval(timer1);
clearInterval(timer2);
var html = document.documentElement;
timer1 = animate(html.scrollTop, 0, (val) => {
html.scrollTop = val;
});
timer2 = animate(html.scrollLeft, 0, (val) => {
html.scrollLeft = val;
});
}
/**
* 在300毫秒之内,从指定的初始值,变化到结束值
* @param {*} start
* @param {*} end
*/
function animate(start, end, callback) {
var tick = 16; //每隔16毫秒完成一次变化
var total = 300; //总时间为1000毫秒
var times = Math.ceil(total / tick); //变化的次数
var curTimes = 0;
var dis = (end - start) / times; //总距离/次数,每次运动的距离
var timer = setInterval(() => {
curTimes++;
start += dis;
if (curTimes === times) {
start = end;
clearInterval(timer);
}
callback(start);
}, tick);
return timer;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 使用 useEffect
DETAILS
import React from "react";
import { BrowserRouter as Router, Route, NavLink } from "react-router-dom";
import "./App.css";
import useScroll from "./useScroll";
function Page1(props) {
useScroll(props.location.pathname);
return <div className="page page1">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam ex at fugiat, vero r</div>;
}
function Page2(props) {
useScroll(props.location.pathname);
return <div className="page page2">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam ex at fugiat, vero ratione ullam sed dignissimos nam pariatur iusto cum, quam</div>;
}
export default function App() {
return (
<Router>
<Route path="/page1" component={Page1} />
<Route path="/page2" component={Page2} />
<div className="nav">
<NavLink to="/page1">页面1</NavLink>
<NavLink to="/page2">页面2</NavLink>
</div>
</Router>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
useScroll.js
import { useEffect } from "react";
import reset from "./resetScroll";
export default function useScroll(pathname) {
useEffect(reset, [pathname]);
}
2
3
4
5
6
# 使用自定义的导航守卫
DETAILS
import React from "react";
import { Route, NavLink } from "react-router-dom";
import RouteGuard from "./RouteGuard";
import "./App.css";
import reset from "./resetScroll";
function Page1(props) {
return <div className="page page1">Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam ex at fugiat, vero ratione ullam sed dignissimos nam pariatur iusto cum, quam eos ab vitae</div>;
}
function Page2(props) {
return (
<div className="page page2">
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam ex at fugiat, vero ratione ullam sed dignissimos nam pariatur iusto cum, quam eos ab vitae voluptas voluptate minu
</div>
);
}
export default function App() {
return (
<RouteGuard
onChange={(prevLocation, location) => {
if (prevLocation.pathname !== location.pathname) {
reset();
}
}}
>
<Route path="/page1" component={Page1} />
<Route path="/page2" component={Page2} />
<div className="nav">
<NavLink to="/page1">页面1</NavLink>
<NavLink to="/page2">页面2</NavLink>
</div>
</RouteGuard>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
DETAILS