React路由背景介绍
背景介绍
多页面应用与SPA单页面应用
多页面应用
先说传统的多页面,需要写多个子页面
点击导航栏,整个页面都会刷新,但是实际上我只想刷新一小块的内容,其他东西变化不大
而且这个单页面,每次切换完还需要更新一点内容
就像导航栏,每次跳转完后,href地址也要有小幅变化
但是大部分东西是相似的,这资源利用太差了
所以就有了SPA应用
SPA应用
单页Web应用(single page web application,SPA)。
整个应用只有一个完整的页面。
点击页面中的链接不会刷新页面,只会做页面的局部更新。
数据都需要通过ajax请求获取, 并在前端异步展现。
单页面、多组件
SPA应用的实现就需要依靠于路由
它比传统的 Web 应用程序更快,因为它们在 Web 浏览器本身而不是在服务器上执行逻辑。在初始页面加载后,只有数据来回发送,而不是整个 HTML,这会降低带宽。它们可以独立请求标记和数据,并直接在浏览器中呈现页面,这就是SPA的优势,可以很好的节约带宽以及开发组件成本。
对于路由的理解
什么是路由?
一个路由就是一个映射关系(key:value)
key为路径, value可能是function(运行某个函数)或component(请求某个组件)
路由为前端路由和后端路由
前端路由:
- 浏览器端路由,value是component,用于展示页面内容。
- 注册路由:
<Route path="/test" component={Test}>
- 工作过程:当浏览器的path变为/test时, 当前路由组件就会变为Test组件
- 前端路由也要靠BOM 上的 history
后端路由:
- 理解: value是function, 用来处理客户端提交的请求。
- 注册路由:
router.get(path, function(req, res){})
- 工作过程:当node接收到一个请求时, 根据请求路径找到匹配的路由, 调用路由中的函数来处理请求, 返回响应数据
关于router里面的value是function的解释,触发url之后就需要走这个function
路由实现的基础原理
这部分必须了解,但是不需要实操
前端路由的实现主要依靠于BOM对象的history属性,也就是浏览器历史记录
其次,用浏览器的历史记录是用栈保存的,所以有(LIFO 后进先出)
BOM(Browser Object Model) 是指浏览器对象模型,下属有这几个属性可以操作
window对象 ,是JS的最顶层对象,其他的BOM对象都是window对象的属性
location对象,浏览器当前URL信息
navigator对象,浏览器本身信息
screen对象,客户端屏幕信息
history对象,浏览器访问历史信息
DOM(Document Object Model),网页文档对象模型,这个在这里不算重点就不多展开了
首先可以通过API对BOM对象进行操作,比如这样:
let history = History.createBrowserHistory() //方法一,直接使用H5推出的history身上的API
但是这样操作不太好,所以有一个其他的JS库可以帮忙实现操作DOM
引入js代码
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
这里放几个操作
在 H5 中新增了 createBrowserHistory
的 API ,用于创建一个 history 栈,允许我们手动操作浏览器的历史记录
新增 API:pushState
,replaceState
,原理类似于 Hash 实现。 用 H5 实现,单页路由的 URL 不会多出一个 #
号,这样会更加的美观
<a href="http://www.atguigu.com" onclick="return push('/test1') ">push test1</a><br><br>
<button onClick="push('/test2')">push test2</button><br><br>
<button onClick="replace('/test3')">replace test3</button><br><br>
<button onClick="back()"><= 回退</button>
<button onClick="forword()">前进 =></button>
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
<script type="text/javascript">
// let history = History.createBrowserHistory() //方法一,直接使用H5推出的history身上的API
let history = History.createHashHistory() //方法二,hash值(锚点)
function push (path) {
// 页面跳转,栈中添加一条记录
history.push(path)
// return false就无法进行跳转,只能发现url发生了变化。true的话就可以跳转了
return false
}
function replace (path) {
// 页面跳转,但是栈中不会添加一条记录,而是替换当前的记录
history.replace(path)
}
function back() {
// 相当于浏览器回退按钮按下
history.goBack()
}
function forword() {
// 相当于浏览器前进按钮按下
history.goForward()
}
history.listen((location) => {
// 监听导航栏url变化
console.log('请求路由路径变化了', location)
})
</script>
以上就是前端路由所依靠的技术基础
React Router5路由(已过时)
ReactRouter5版本,已经过时,现已经有router6版本。
这里学习的目的是温故知新,为后面衔接router6做准备
版本选择
非常重要的一个知识,项目里开发用到的是react-router-dom
。react-router-dom
分了不同的版本实现,是给不同平台去用的
- 网页应用 web (用这个)
- React native(原生应用)
- 通用 Anywhere
路由:是路由器上的一根根天线
路由器:是用来管理路由的,也就管理那一根根天线的,我们自己买一大堆天线路由,没有路由器把他管理起来,就是不能用的。这里监听url变化的就是前端路由器
安装
路由-DOM(5版本)
// npm安装路由
npm i react-router-dom@5
// 查看版本
npm react-router-dom -v
路由的基本使用
react-router-dom 专门给 web 人员使用的库
- 一个 react 的仓库
- 很常用,基本是每个应用都会使用的这个库
- 专门来实现 SPA 应用
首先我们要明确好页面的布局 ,分好导航区、展示区。分区完毕之后再加路由等等…
要引入 react-router-dom
库,暴露一些属性 Link(相当于之前的a标签)、BrowserRouter(路由器包裹标签)...
import { Link, Route, BrowserRouter } from 'react-router-dom'
Link标签
导航区的 a 标签改为 Link 标签
// 这里只放对比
<div className="list-group">
{/* 原生是靠a标签跳转,我们要用Link 进行替换 */}
<a className="list-group-item" href="./about.html">
About
</a>
<Link className="list-group-item" to="/about">
About
</Link>
</div>
Link标签会让url变化,只要url变化,就会被路由器监听到,发现变化进行组件跳转
(这里隐藏了Route标签包裹的问题,下面马上就会提到)
Route(路由)标签
import Home from "./componment/routerDemo/home/Home";
import About from "./componment/routerDemo/about/About";
<div className="panel-body">
{/* 注册路由 */}
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
</div>
我们需要用 Route
标签,来进行路径的匹配(访问到了这个url就加载这个路由),从而实现不同路径的组件切换。
点击别的Link这块的组件就会发生变化
Router(路由器)标签
Router是一个大类,类似Java的接口,下属两个标签:<BrowserRouter>
或<HashRouter>
router除了做组件匹配,还有一个非常重要的功能,就是做路由的路由器(管理)。他就像一个接口,下面有两个不同的实现路由器
两个路由器最大的区别就是url上面,至于二者的来源,就是之前提到的浏览器BOM的history对象
-
<BrowserRouter>
对应的url就是http://localhost:3000/about
,没有#
号 -
<HashRouter>
对应的url就是http://localhost:3000/about#/about
,有#
号
所以不难发现,对于用户来说,<BrowserRouter>
没有#
号的体验最好
之前的代码一直是给<BrowserRouter>
包裹路由器问题忽略了,实际上如果不包,运行代码,此时就会发现,Link外面需要有路由器标签包裹(这里指的子标签BrowserRouter或HashRouter,用Router标签包是不行的)
因此我们也可以在 Link 和 Route 标签的外层标签采用 BrowserRouter 包裹,但是这样当我们的路由过多时,我们要不停的更改标签包裹的位置,因此我们可以这么做,找到最顶层标签然后用路由器标签包裹(<BrowserRouter>
或<HashRouter>
)
我们回到 App.jsx 目录下的 index.js 文件,将整个 App 组件标签采用 BrowserRouter
标签去包裹,这样整个 App 组件都在一个路由器的管理下,
// index.js
<BrowserRouter>
< App />
</BrowserRouter>
Router(路由器)的反例
反例就是把Link和Route放在不同的路由器下面,导致两个路由器不互通,就失去了路由的意义
路由组件和普通组件的区别
1.写法不同:
一般组件:<Demo/>
路由组件:<Route path="/demo" component={需要导入的组件}/>
为了规范我们的书写,一般将路由组件放在 pages
文件夹中,路由组件放在 components
我们指的路由组件,是在<Route/>
这个标签里面component
导入的标签(也就是匹配url变动的标签)
2.存放位置不同:
一般组件:components
路由组件:pages
3.接收到的props不同:
一般组件:写组件标签时传递了什么,就能收到什么
路由组件:接收到三个固定的属性(history,location,match)
运行代码输出一下,可以看到路由组件默认会有非常多的参数,但是对于开发人员来说只用到一部分
在普通组件中,如果我们不进行传递,就不会收到值。而对于路由组件而言,它会接收到 3 个固定属性 history
、location
以及 match
路由组件里常用的参数,这些在后面的编程式路由导航有很大用:
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/about"
search: ""
state: undefined
match:
params: {}
path: "/about"
url: "/about"
NavLink
NavLink 标签是和 Link 标签作用相同的,但是它又比 Link 更加强大。
在前面的 demo 展示中,你可能会发现点击的按钮并没有出现高亮的效果,正常情况下我们给标签多添加一个 active
的类就可以实现高亮的效果
而 NavLink 标签正可以帮助我们实现这一步
当我们选中某个 NavLink 标签时,就会自动的在类上添加一个 active
属性
activeClassName属性
我们可以看到左侧的元素类名在不断的切换,当然 NavLink 标签是默认的添加上 active
类,我们也可以改变它,在标签上添加一个属性 activeClassName
例如 activeClassName="自定义css"
在触发这个 NavLink 时(点击了),会自动添加一个 自定义css
在css文件里加入的属性
.btn-active {
color: #fff;
background-color: rgb(47, 189, 71); # 绿的
}
jsx的NaviLink
import "./App.css";
<NavLink
className="list-group-item"
activeClassName="btn-active !important"
to="/about"
>
{/* className 是未被选中的状态下的css
activeClassName 是被选中状态下的css 加!important是为了提升css优先级*/}
About
</NavLink>
NavLink二次封装
在上面的 NavLink 标签中,我们可以发现我们每次都需要重复的去写这些样式名称或者是 activeClassName
,这并不是一个很好的情况,代码过于冗余。再比如我们想封装一点自己的属性进去,就可以对NavLink进行二次封装,搞一个自定义的标签。
我们可以自定义一个 MyNavLink
组件,对 NavLink 进行封装
比如这样:
<MyNavlink to="/about">About</MyNavlink>
等价于
<MyNavlink to="/about" children={"About"} />
注意,标签体内容也是一个特殊的标签属性
<MyNavlink to="/about">About</MyNavlink>
这里面夹着的About属性
是标签体内部的值,是可以作为props属性传入的
如果是自闭合标签,不直接传值,用children属性传效果是一样的
<MyNavlink to="/about" children={"About"} />
比如我在自定义组件内部打印一下,就能看到这个值
固定字段children
属性就保存着我们的标签体内容
尝试给自定义的NavLink里传入大量参数
调用方
<MyNavlink to="/about" a={1} b={2} c={"c"}>
About
</MyNavlink>
自定义标签方
这个时候就会有个问题,像这样用解构去挨个接收参数很麻烦
const { a, b, c } = this.props;
所以咱们直接不解构了,直接传进去的
上面是调用传入的参数
<MyNavlink to="/about" a={1} b={2} c={"c"}>
About
</MyNavlink>
这里用{...this.props} 给上面的[to="/about" a={1} b={2} c={"c"}]这些
全部给扒过来,连带key和value一起给带到NavLink里面(包括那个标签体内的About)
//这里一定要用展开符展开一下,要不然解不开报错
<NavLink className="list-group-item" {...this.props} />
在里面打印下,看一下传入效果,全部k-v都传过来了
Switch标签解决同路径匹配问题
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
<Route path="/home" component={Test}></Route>
这是两个路由组件,在2,3行中,我们同时使用了相同的路径 /home
匹配时,就会出现两个组件同时显示的情况(这两个组件如果想一起显示明明可以放在一个组件里面)
所以,这个案例就说明,路由做匹配是匹配所有的Route标签,如果是大项目里面一大堆的Route标签,挨个匹配就会降低性能
所以引出Switch标签
匹配到了之后,后面的就不会再匹配了,从而节约性能
import { Switch } from "react-router-dom";
<Switch>
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
<Route path="/home" component={Test}></Route>
</Switch>
解决二级路由样式丢失的问题
之前的路径一直是http://localhost:3000/home
这种的一级路径
那如果是二级路径http://localhost:3000/test/home
这种的二级路径就会造成样式丢失的问题
其根本原因,是因为css文件在脚手架的public目录下,写成了相对路径,当路径发生变化的时候,因为是相对路径,所以前缀就会带上test
的路径,导致找不到文件而返回默认的index.html
解决方案
将样式引入的路径改成绝对路径,不带 ./
css文件
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>react脚手架</title>
//绝对路径
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/bootstrap.css" />
</head>
...
</html>
引入样式文件时用%PUBLIC_URL%
%PUBLIC_URL%代表脚手架路径,就是http://localhost:3000/test/home
里面的http://localhost:3000
所以这个就是绝对路径,就不会再丢失了
使用 HashRouter (不常用,用户体验不好)
http://localhost:3000/#/test/home
#号后面的内容都被认为是前端资源,根本不带给服务器。#号后面的路径就自动忽略了,所以就不存在丢失问题
此时的css无论是相对路径或者是绝对路径就都无所谓了
css:
<link rel="icon" href="./favicon.ico" />
<link rel="stylesheet" href="./css/bootstrap.css" />
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<HashRouter>
<App />
</HashRouter>
);
在实际开发中,一般常用绝对路径,HashRouter很少用
路由的精准匹配和模糊匹配
路由的匹配有两种形式,一种是精准匹配一种是模糊匹配,React 中默认开启的是模糊匹配
-
模糊匹配可以理解为,只要路径顺序能匹配上并且url范围比路由path大,就可以匹配上
-
精准匹配就是,匹配路由时,两者必须相同
我们展示一个模糊匹配的例子
<NavLink className="list-group-item" to="/home/a/b">
Home
</NavLink>
//路由path,只要是顺序能对上就可以触发路由,所以下面两个路由匹配[/home/a/b]都可以匹配上
<Route path="/home" component={Home}></Route>
<Route path="/home/a" component={Home}></Route>
//但是这种,超出了范围的就无法匹配了
<Route path="/home/a/b/c" component={Home}></Route>
简单来说,想要触发模糊匹配需要:
路径顺序能匹配 + url的范围小于Route的path范围
就可以触发。这么设计是为了后面react路由的匹配,所以开发中一般用的都是模糊匹配
无法模糊匹配的例子
<NavLink className="list-group-item" to="/home/a/b">
Home
</NavLink>
//路由path
<Route path="/home" component={Home}></Route>
<Route path="/home/a" component={Home}></Route>
//这种超出了范围的就无法匹配
<Route path="/home/a/b/c" component={Home}></Route>
//这种顺序错了的也无法匹配
<Route path="/a/home/b" component={Home}></Route>
尝试开启精准路由
当我们开启了精准匹配后,就我们的第一种匹配就不会成功,因为精准匹配需要的是完全一样的值,开启精准匹配采用的是 exact
来实现,效果就不展示了,必须url完全匹配上路径才可以
<Route exact={true} path="/home" component={Home}/>
请注意,精准匹配不要轻易开启,很容易造成路由嵌套无法匹配的问题,如果不是影响到项目开发,就不要开启精准路由
Redirect重定向路由
在我们写好了这些之后,我们会发现,我们需要点击任意一个按钮,才会去匹配一个组件,这并不是我们想要的,我们想要页面一加载上来,默认的就能匹配到一个组件。
这个时候我们就需要时候 Redirecrt 进行默认匹配了,属于兜底的方法
只要是没有匹配到 /about
或者 /home
,就会默认去Redirect
标签标记的home 路径
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
<Redirect to="/home" />
比如url输入http://localhost:3000/home1
或者是一进来页面的默认的http://localhost:3000/
路径,都会被Redirect标签给路由到兜底到/home
下
也就是http://localhost:3000/home
嵌套路由
先看一个情景,home组件里面又套两个Link切换的组件,这种情况就需要用嵌套路由来解决
- 一级路由:
/home
- 二级路由:
/home/About
- 三级路由:
/home/About/Test
- 多级路由:
/.../.../.../.../...
其实简单来说,就是把想要展示的组件,用多级路由嵌套出来一个唯一url,来匹配这个组件
以下是App.jsx组件的内容,用NavLink来展示Home组件
class App extends React.Component {
render() {
return (
<div>
<div className="row">
<NavLink className="list-group-item" to="/about">
About
</NavLink>
<NavLink className="list-group-item" to="/home">
Home
</NavLink>
</div>
<div className="panel">
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
</div>
</div>
);
}
}
export default App;
我们将我们的嵌套内容写在相应的组件里面,这个是在 Home 组件的 return 内容
<div>
<h2>Home组件内容,</h2>
<div>
<ul className="nav nav-tabs">
<li>
<MyNavLink className="list-group-item" to="/home/news">News</MyNavLink>
</li>
<li>
<MyNavLink className="list-group-item " to="/home/message">Message</MyNavLink>
</li>
</ul>
{/* 注册路由 */}
<Switch>
<Route path="/home/news" component={News} />
<Route path="/home/message" component={Message} />
</Switch>
</div>
</div>
这里提一下开发的包规范:Home组件里嵌套的News组件和Message组件,可以放在Home的包下面
或者用在同级别的包下,用父组件_子组件
的包名也可以。总之各个公司不一样,可以根据实际情况再去探讨
在这里我们需要使用嵌套路由的方式,才能完成匹配
首先我们得 React 中路由得注册是有顺序的,在匹配得时候,因为 Home 组件是先注册得,因此在匹配的时候先去找 home 路由,由于是模糊匹配,会成功的匹配
在 Home 组件里面去匹配相应的路由,从而找到 /home/news 进行匹配,因此找到 News 组件,进行匹配渲染
如果开启精确匹配的话,第一步的
/home/news
匹配/home
就会卡住不动。后面写的/home/news
这个时候就不会被匹配到了
当前标题下挤压太多东西了,这里给传参部分单独起一个大标题
路由参数传递(声明式路由导航)
param传递参数
可以在url里面传递参数,做出的效果就是点击开一个嵌套路由,就可以把参数从Link里面传给嵌套的路由。
这种参数传递就是依靠于url上拼接参数
我们可以通过将数据拼接在路由地址末尾来实现数据的传递
这里我们需要注意的是:需要采用模板字符串以及
$
符的方式来进行数据的获取
<Link to={`/home/message/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
如上,我们将消息列表的 id 和 title 写在了路由地址后面
在注册路由时,可以在路由上的url通过 :数据名
来接收数据
<Route path="/home/message/:id/:title" component={Message} />
如上,使用了 :id/:title
成功的接收了Link 传递过来的 id 和 title 数据
(从App组件,传递给Home组件下的子组件Message)
这样我们既成功的实现了路由的跳转,又将需要获取的数据传递给了 Detail 组件
我们在 Message组件中打印 this.props
来查看当前接收的数据情况
我们可以发现,我们传递的数据被接收到了对象的 match 属性下的 params 中
因此我们可以在 Message 组件中获取到传递来的 params 数据
并通过 params 数据中的 id
值,在详细内容的数据集中查找出指定 id
的详细内容,通过解构等方式来实现
const { id, title } = this.props.match.params;
最后直接使用渲染数据即可
总结一下优缺点:
- 优点:刷新页面,参数不丢失
- 缺点:1.只能传字符串,传值过多url会变得很长 2. 参数必须在路由上配置
search传递参数
我们还可以采用传递 search 参数的方法来实现
首先我们先确定数据传输的方式
我们先在 Link 中采用 ?
符号的方式来表示后面的为可用数据
<Link to={`/home/message/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
采用 search
传递的方式,无需在 Route 中再次声明,因为是通过url的后缀进行传递的,类似这种:
https://csdn.net/home/message/?id=${msgObj.id}&title=${msgObj.title}
采用 search
传递的方式,无需在 Route标签中再和以前一样声明接收参数。转而在接受参数的组件中处理
打印一下参数我们可以看到,在location里面可以看到传入的参数(实际上就是url)
此时的url为:http://localhost:3000/home/message?id=id&title=title
那这个url里面的参数如果需要获取,就得借助工具来进行转换,这里选择了querystring
querystring引入时报错解决
import qs from "qs";
这个库是 React 中自带有的,它有两个方法,一个是 `parse` 一个是 `stringify`
我们可以采用 `parse` 方法,将字符串转化为键值对形式的对象
// 解构获取location路径
const { search } = this.props.location;
const { id, title } = qs.parse(search.slice(1));
这样我们就能成功的获取数据,并进行渲染
其实以上两种都适用于对于数据并不敏感的场景,或者一些简单的数据传输,并不适合大规模的数据传输,并且因为参数是对外暴露在url的,所以并不是很安全。同时url会挂着一大堆的参数,会很长,这样不太好,由此就可以引出我们第三种数据的传递方式通过state进行传输。
总结一下优缺点:
- 优点:刷新页面,参数不丢失
- 缺点:1.只能传字符串,传值过多url会变得很长 2. 获取参数需要解构
this.props
state传递参数
通过state进行参数传递,有效避免了数据暴露在url上的问题,采用内部的状态(state)来维护,在to里进行传递。
const obj = { id: "id", title: "title" };
<Link
className="list-group-item"
to={{
pathname: "/home/message",
state: { id: obj.id, title: obj.title }
}}
>
Home
</Link>
// route里面不再需要单独标记数据接收
<Route path="/home/message/" component={Message}></Route>
可以看到,数据已经通过this.props
里的location.state
传入组件,在组件中解构一下
直接解构完拿着用即可
const { id, title } = this.props.location.state;
如果传入的数据为空的话,解构就会出现undefined的情况
总结一下优缺点:
- 优点:可以传对象
- 缺点: 刷新页面,参数丢失
解决清除缓存造成报错的问题,可以在获取不到数据的时候用空对象来替代,例如,
const { id, title } = this.props.location.state || {}
当获取不到 state
时,则用空对象代替
这里的 state 和状态里的 state 有所不同
Push与Replace
Push
路由中,默认情况下,开启的是 push 模式,也就是说,每次点击跳转,都会向栈中压入一个新的地址,在点击返回时,可以返回到上一个打开的地址(所有的history全部记录)
就像上图一样,我们每次返回都会返回到上一次点击的地址中
Replace
有时候我们可能会不喜欢这种繁琐的跳转,我们可以开启 replace 模式,这种模式与 push 模式不同,它会将当前地址替换成点击的地址,也就是替换了新的栈顶。也就是无论切换了多少次url,因为是replace模式,所以history里面只有最新的这个url
我们只需要在需要开启的<Link/>
上加上 replace
即可
<Link replace
className="list-group-item"
to={{
pathname: "/home/message",
state: { id: obj.id, title: obj.title },
}}
>
Home
</Link>
编程式路由导航
React中,导航分为两种,声明式导航和命令式导航。
- 声明式导航:之前我们直接写在代码里标记好的
<Link/>
标签,就是声明式的导航,只标明了一些属性,依靠属性来完成某些操作 - 命令式导航:采用绑定事件的方式实现路由的跳转,我们在按钮上绑定一个
onClick
事件,当事件触发时,我们执行replaceShow
函数,在方法体里通过操作this对象来改变url实现跳转操作this.props.history.replace(/home/message/${id}/${title})
之前的声明式导航,只有一个Link或者NavLink,自定义程度太低了,而且如果想搞其他功能只能二次封装。
但是用按钮完成跳转、定时任务跳转、点图片跳转…这些需求都没办法用声明式导航实现
如果想用编程式导航,就必须用路由组件
因为只有路由组件,props里才有这些编程式导航用的API来操作url,否则就没意义了
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/about"
search: ""
state: undefined
match:
params: {}
path: "/about"
url: "/about"
路由组件有两种
- 一种是
<Route path="/home/news" component={News}>
Route标签component组件指定的 - 另一种就是用React自带API
withRouter(普通组件)
来把普通组件处理成路由组件
例子:在路由组件里用编程式导航,点击就可以完成Link一样的效果
export default class News extends Component {
// News是一个被Route标记了的路由组件!!!!!!!!
moveToNextPage = () => {
// 因为props里有history对象,所以就可以操作url
this.props.history.replace(`/home/news/detail`);
push,replace两种操作都可以修改url,区别就是回退时是否有记录
this.props.history.push(`/home/news/detail`);
};
render() {
return (
<div>
News
<button onClick={this.moveToNextPage}>前往下一个组件</button>
<Route path="/home/news/detail" component={Detail}></Route>
</div>
);
}
}
反例:在普通组件里面使用history对象操作
这里不放太多代码,就是在App这个普通组件里打印一下props,结果就是什么都没有,更不用说后续操作history对象了
class App extends React.Component {
render() {
console.log(this.props);
}
}
编程式路由导航传参
我们先看一下api
push
和replace
这两个方法都是可以传state参数的,所以对比于传统的Link声明式导航传参,编程式导航依旧可以传参
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
push和replace都是改变url的手段,区别就在于history的操作,例子就不纠结用哪个了,随便抓一个实现案例即可
编程式路由导航param传参
这个本质就是修改url进行的传参,方式和上面的param几乎完全一样,就是改了一下Link
class App extends React.Component {
replaceShow = (id, title) => {
// 这里注意用箭头函数向外扩散找this对象,否则会造成死循环
// 通过变更url传参
return () => this.props.history.push(`/home/message/${id}/${title}`);
};
render() {
const obj = { id: "id", title: "title" };
return (
<div>
<div className="row">
<Header></Header>
<div className="col-xs-2 col-xs-offset-2">
// 传入参数
<button onClick={this.replaceShow(obj.id, obj.title)}>
命令式导航
</button>
</div>
</div>
// 路由注册参数
<Route
path="/home/message/:id/:title"
component={Message}
></Route>
</div>
);
}
}
export default App;
// 接收参数的组件直接解构即可
export default class Message extends Component {
render() {
// 解构获取location路径
const { id, title } = this.props.match.params;
console.log(id, title);
return <div></div>;
}
}
编程式路由导航search传参
class App extends React.Component {
replaceShow = (id, title) => {
// 这里注意用箭头函数向外扩散找this对象,否则会造成死循环
// 通过变更url传参
return () => this.props.history.push(`/home/message/?id=${id}&title=${title}`);
};
render() {
const obj = { id: "id", title: "title" };
return (
<div>
<div className="row">
<Header></Header>
<div className="col-xs-2 col-xs-offset-2">
// 传入参数
<button onClick={this.replaceShow(obj.id, obj.title)}>
命令式导航
</button>
</div>
</div>
// 这里不再需要去注册参数了
<Route
path="/home/message"
component={Message}
></Route>
</div>
);
}
}
export default App;
// 接收参数的组件,qs查完直接解构即可
import qs from "qs";
export default class Message extends Component {
render() {
// 解构获取location路径
const { search } = this.props.location;
// 用qs给search传进来的参数查出来
const { id, title } = qs.parse(search.slice(1));
console.log(search);
console.log(id, title);
return <div></div>;
}
}
从url里面解构出来挂着的参数,可以看到能够获取到并且打印出来
编程式路由导航state传参
看History下的这俩API,直接就能传state,直接往里面传值就可以了。不需要修改url和路由
和之前的state传值其实是一样的
history:
push: ƒ push(path, state)
replace: ƒ replace(path, state)
class App extends React.Component {
replaceShow = (id, title) => {
// 这里注意用箭头函数向外扩散找this对象,state传值不再需要修改url
return () => this.props.history.push(`/home/message`, { "id": id, "title": title });
};
render() {
const obj = { id: "id", title: "title" };
return (
<div>
<div className="row">
<Header></Header>
<div className="col-xs-2 col-xs-offset-2">
// 传入参数
<button onClick={this.replaceShow(obj.id, obj.title)}>
命令式导航
</button>
</div>
</div>
// 这里不再需要去注册参数了
<Route
path="/home/message"
component={Message}
></Route>
</div>
);
}
}
export default App;
export default class Message extends Component {
render() {
// 解构获取state里的数据
const { id, title } = this.props.location.state;
console.log(this.props.location);
console.log(id, title);
return <div></div>;
}
}
编程式路由导航总结
借助this.props.history对象上的API对操作路由跳转、前进、后退
go: ƒ go(n) // n传入几就前进几步,传入正数前进,传入负数后退
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state) // path是url路径,state是传值
replace: ƒ replace(path, state) // path是url路径,state是传值
withRouter
当我们需要在页面内部添加回退前进等按钮时,由于这些组件我们一般通过一般组件的方式去编写,因此我们会遇到一个问题,无法获得 history 对象,这正是因为我们采用的是一般组件造成的。
普通组件去打印history对象是打印不出来的
class Header extends Component {
// 这是个普通组件
render() {
// log一下参数
console.log("普通组件输出参数", this.props.history);
return <div>React-Router-Demo Header</div>;
}
}
export default Header;
只有路由组件才能获取到 history 对象,普通组件是获取不到history对象,所以是个undefined
这里就可以用React路由自带的withRouter
来对普通组件包装一下,使其变为路由组件
但是注意一个非常重要的东西,就是react-router-dom自带的withRouter首字母是小写!
在普通组件里面引入一下withRouter
import React, { Component } from "react";
// 引入withRouter
import { withRouter } from "react-router-dom";
class Header extends Component {
// 这是个普通组件
render() {
// log一下参数
console.log("普通组件输出参数", this.props.history);
return <div>React-Router-Demo Header</div>;
}
}
// 在最后导出对象时,用 `withRouter` 函数对 Header进行包装
export default withRouter(Header);
此时可以看到普通组件Header已经被改造成了路由组件
BrowserRouter 和 HashRouter 的区别
1.底层原理不一样:
BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
HashRouter使用的是URL的哈希值。
2.path表现形式不一样
BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
HashRouter的路径包含#,例如:localhost:3000/#/demo/test
3.刷新后对路由state参数的影响
(1).BrowserRouter没有任何影响,因为state保存在history对象中。
(2).HashRouter刷新后会导致路由state参数的丢失!!!
4.备注:HashRouter可以用于解决一些路径错误相关的问题。
为什么HashRouter刷新之后就会丢失state?
当使用 HashRouter 时,刷新页面可能会导致丢失 state 的原因在于它的工作原理。每次页面刷新时,浏览器都会重新加载页面和相关的资源,包括 JavaScript 代码和静态资源。由于 HashRouter 是通过 JavaScript 来处理路由的,因此在刷新页面时,JavaScript 代码需要重新加载,而之前保存的状态也会被清除。history丢失了,自然也就没有state了。