React路由
路由是根据不同的 URL 地址展示不同的内容或页面
在 SPA 应用中,大部分页面结果不改变,只改变部分内容的使用
优点:
- 用户体验好,不需要每次都从服务器全部获取整个 HTML,快速展现给用户
缺点:
- SPA 无法记住之前页面滚动的位置,再次回到页面时无法记住滚动的位置
- 使用浏览器的前进和后退键会重新请求,没有合理利用缓存
路由的原理
前端路由的主要依靠的时 history ,也就是浏览器的历史记录
history 是 BOM 对象下的一个属性,在 H5 中新增了一些操作 history 的 API
浏览器的历史记录就类似于一个栈的数据结构,前进就相当于入栈,后退就相当于出栈
并且历史记录上可以采用 listen 来监听请求路由的改变,从而判断是否改变路径
在 H5 中新增了 createBrowserHistory 的 API ,用于创建一个 history 栈,允许我们手动操作浏览器的历史记录
新增 API:pushState ,replaceState,原理类似于 Hash 实现。 用 H5 实现,单页路由的 URL 不会多出一个 # 号,这样会更加的美观
路由的基本使用
npm install react-router-dom@5
react-router-dom
专门给 web 人员使用的库
- 一个 react 的仓库
- 很常用,基本是每个应用都会使用的这个库
- 专门来实现 SPA 应用
要引入 react-router-dom
库,暴露一些属性 Link、BrowserRouter...
import { Link, BrowserRouter, Route } from 'react-router-dom'
我们项目中的 a 标签改为 Link 标签
<Link className="list-group-item" to="/about">About</Link>
在我们需要路由切换的部分中,使用Route
标签,来进行路径的匹配,从而实现不同路径的组件切换
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
这样之后我们还需要一步,加个路由器,在上面我们写了两组路由,同时还会报错指示我们需要添加 Router 来解决错误,这就是需要我们添加路由器来管理路由,如果我们在 Link 和 Route 中分别用路由器管理,那这样是实现不了的,只有在一个路由器的管理下才能进行页面的跳转工作。
因此我们也可以在 Link 和 Route 标签的外层标签采用 BrowserRouter 包裹,但是这样当我们的路由过多时,我们要不停的更改标签包裹的位置,因此我们可以这么做
我们回到 App.jsx 目录下的 index.js 文件,将整个 App 组件标签采用 BrowserRouter 标签去包裹,这样整个 App 组件都在一个路由器的管理下
// index.js
<BrowserRouter>
< App />
</BrowserRouter>
路由组件和一般组件
在我们前面的内容中,我们是把组件 Home 和组件 About 当成是一般组件来使用,我们将它们写在了 src 目录下的 components 文件夹下,但是我们又会发现它和普通的组件又有点不同,对于普通组件而言,我们在引入它们的时候我们是通过标签的形式来引用的。但是在上面我们可以看到,我们把它当作路由来引用时,我们是通过 {Home} 来引用的。
从这一点我们就可以认定一般组件和路由组件存在着差异
首先它们的写法不同
一般组件:<Demo/>
,路由组件:<Route path="/demo" component={Demo}/>
同时为了规范我们的书写,一般将路由组件放在 pages 文件夹中,路由组件放在 components
而最重要的一点就是它们接收到的 props 不同,在一般组件中,如果我们不进行传递,就不会收到值。而对于路由组件而言,它会接收到 3 个固定属性 history 、location 以及 match
NavLink标签
当我们选中某个 NavLink 标签时,就会自动的在类上添加一个 active
属性
<NavLink className="list-group-item" to="/about">About</NavLink>
NavLink 标签是默认的添加上 active
类,我们也可以改变它,在标签上添加一个属性 activeClassName
例如 activeClassName="aaa"
在触发这个 NavLink 时,会自动添加一个 aaa
类
NavLink封装
在上面的 NavLink 标签种,我们可以发现我们每次都需要重复的去写这些样式名称或者是 activeClassName
,这并不是一个很好的情况,代码过于冗余。那我们是不是可以想想办法封装一下它们呢?
我们可以对 NavLink 进行封装
首先我们需要新建一个 MyNavLink 组件
return
一个结构
<NavLink className="list-group-item" {...this.props} />
首先,有一点非常重要的是,我们在标签体内写的内容都会成为一个 children
属性,因此我们在调用 MyNavLink
时,在标签体中写的内容,都会成为 props
中的一部分,从而能够实现
接下来我们在调用时,直接写
<MyNavLink to="/home">home</MyNavLink>
即可实现相同的效果
Switch 解决相同路径问题
首先我们看一段这样的代码
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/about" component={About}></Route>
这是两个路由组件,在2,3行中,我们同时使用了相同的路径 /about
我们发现它出现了两个 about
组件的内容
因为
Route
的机制,当匹配上了第一个/about
组件后,它还会继续向下匹配,因此会出现两个 About 组件,这时我们可以采用Switch
组件进行包裹
<Switch>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
<Route path="/about" component={About}></Route>
</Switch>
在使用 Switch
时,我们需要先从 react-router-dom
中暴露出 Switch
组件
这样我们就能成功的解决掉这个问题了
解决二级路由样式丢失的问题
当我们将路径改写成 path=“/ljc/about” 这样的形式时,我们会发现当我们强制刷新页面的时候,页面的 CSS 样式消失了。这是因为,我们在引入样式文件时,采取的是相对路径,当我们使用二级路由的时候,会使得请求的路径发生改变,浏览器会向 localhost:3000/ljc 下请求 css 样式资源,这并不是我们想要的,因为我们的样式存放于公共文件下的 CSS 文件夹中。
我们有几种方法,可以解决这个问题
- 将样式引入的路径改成绝对路径
- 引入样式文件时不带
.
- 使用 HashRouter
我们一般采用第一种方式去解决
这里我们简单介绍一下如果使用HashRouter
import {
BrowserRouter as Router,
Route,
Link
} from 'react-router-dom';
//改为
import {
HashRouter as Router,
Route,
Link
} from 'react-router-dom';
路由的精准匹配和模糊匹配
路由的匹配有两种形式,一种是精准匹配,一种是模糊匹配,React 中默认开启的是模糊匹配
- 模糊匹配可以理解为,在匹配路由时,只要有匹配到的就好了
- 精准匹配就是,两者必须相同
我们展示一个模糊匹配的例子
<MyNavLink to = "/home/a/b" >Home</MyNavLink>
这个标签匹配的路由,我们可以拆分成 home a b,将会根据这个先后顺序匹配路由
<Route path="/home"component={Home}/>
就可以匹配到上面的这个路由,因为它匹配的是 home
当匹配的路由改成下面这样时,就会失败。它会按照第一个来匹配,如果第一个没有匹配上,那就会失败,这里的 a 和 home 没有匹配上,很显然会失败
<Route path="/a" component={Home}/>
当我们开启了精准匹配后,就我们的第一种匹配就不会成功,因为精准匹配需要的是完全一样的值,开启精准匹配采用的是 exact
来实现
<Route exact={true} path="/home" component={Home}/>
重定向路由
在我们写好了这些之后,我们会发现,我们需要点击任意一个按钮,才会去匹配一个组件,这并不是我们想要的,我们想要页面一加载上来,默认的就能匹配到一个组件。
这个时候我们就需要时候 Redirect 进行默认匹配了。
<Redirect to="/home" />
当我们加上这条语句时,页面找不到指定路径时,就会重定向到 /home
页面下因此当我们请求3000端口时,就会重定向到 /home
这样就能够实现我们想要的效果了
需要先从 react-router-dom
中暴露出 Redirect
组件
嵌套路由
嵌套路由也就是我们前面有提及的二级路由,但是嵌套路由包括了二级、三级…还有很多级路由,当我们需要在一个路由组件中添加两个组件,一个是头部,一个是内容区
我们将我们的嵌套内容写在相应的组件里面,这个是在 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>
在这里我们需要使用嵌套路由的方式,才能完成匹配
首先我们得 React 中路由得注册是有顺序得,我们在匹配得时候,因为 Home 组件是先注册得,因此在匹配的时候先去找 home 路由,由于是模糊匹配,会成功的匹配
在 Home 组件里面去匹配相应的路由,从而找到 /home/news 进行匹配,因此找到 News 组件,进行匹配渲染
注意!!!!如果开启精确匹配的话,第一步的
/home/news
匹配/home
就会卡住不动,这个时候就不会显示有用的东西了!
路由传参
传递params参数
我们可以通过将数据拼接在路由地址末尾来实现数据的传递
<Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link>
这里我们需要注意的是:需要采用模板字符串以及
$
符的方式来进行数据的获取
在注册路由时,我们可以通过 :数据名
来接收数据
<Route path="/home/message/detail/:id/:title" component={Detail} />
如上,使用了 :id/:title
成功的接收了由 Link 传递过来的 id 和 title 数据
这样我们既成功的实现了路由的跳转,又将需要获取的数据传递给了 Detail 组件
我们在 Detail 组件中打印 this.props
来查看当前接收的数据情况
我们可以发现,我们传递的数据被接收到了对象的 match 属性下的 params 中
因此我们可以在 Detail 组件中获取到又 Message 组件中传递来的 params 数据
传递search参数
我们还可以采用传递 search 参数的方法来实现
首先我们先确定数据传输的方式
我们先在 Link 中采用 ?
符号的方式来表示后面的为可用数据
<Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link>
采用 search
传递的方式,无需在 Route 中再次声明,可以在 Detail 组件中直接获取到
我们可以发现,我们的数据保存在了 location
对象下的 search
中,是一种字符串的形式保存的,我们可以引用一个库来进行转化 querystring
import qs from 'querystring'
这个库是 React 中自带有的,它有两个方法,一个是 parse
一个是 stringify
我们可以采用 parse
方法,将字符串转化为键值对形式的对象
const { search } = this.props.location
const { id, title } = qs.parse(search.slice(1))
这样我们就能成功的获取数据,并进行渲染
传递state参数
采用传递 state 参数的方法,是我觉得最完美的一种方法,因为它不会将数据携带到地址栏上,采用内部的状态来维护
<Link to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
首先,我们需要在 Link 中注册跳转时,传递一个路由对象,包括一个 跳转地址名,一个 state 数据,这样我们就可以在 Detail 组件中获取到这个传递的 state 数据
注意:采用这种方式传递,无需声明接收
我们可以在 Detail 组件中的 location 对象下的 state 中取出我们所传递的数据
const { id, title } = this.props.location.state
直接使用即可
解决清除缓存造成报错的问题,我们可以在获取不到数据的时候用空对象来替代,例如:
const { id, title } = this.props.location.state || {}
注意!!!!这里的 state 和状态里的 state 有所不同
路由跳转
push 与 replace 模式
默认情况下,开启的是 push 模式,也就是说,每次点击跳转,都会向栈中压入一个新的地址,在点击返回时,可以返回到上一个打开的地址。
当我们在读消息的时候,有时候我们可能会不喜欢这种繁琐的跳转,我们可以开启 replace 模式,这种模式与 push 模式不同,它会将当前地址替换成点击的地址,也就是替换了新的栈顶
<Link replace to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>
编程式路由导航
我们可以采用绑定事件的方式实现路由的跳转,我们在按钮上绑定一个 onClick 事件,当事件触发时,我们执行一个回调 replaceShow
这个函数接收两个参数,用来仿制默认的跳转方式,第一个是点击的 id 第二个是标题
我们在回调中,调用 this.props.location 对象下的 replace 方法
replaceShow = (id, title) => {
this.props.history.replace(`/home/message/detail/${id}/${title}`)
}
同时我们可以借助 this.props.history
身上的 API 实现路由的跳转,例如 go
、goBack
、goForward
withRouter
当我们需要在页面内部添加回退前进等按钮时,由于这些组件我们一般通过一般组件的方式去编写,因此我们会遇到一个问题,无法获得 history 对象,这正是因为我们采用的是一般组件造成的。
只有路由组件才能获取到 history 对象
因此我们需要如何解决这个问题呢
我们可以利用 react-router-dom 对象下的 withRouter 函数来对我们导出的 Header 组件进行包装,这样我们就能获得一个拥有 history 对象的一般组件
我们需要对哪个组件包装就在哪个组件下引入
// Header/index.jsx
import { withRouter } from 'react-router-dom'
// 在最后导出对象时,用 `withRouter` 函数对 index 进行包装
export default withRouter(index);
这样就能让一般组件获得路由组件所特有的 API
BrowserRouter 和 HashRouter 的区别
它们的底层实现原理不一样
对于 BrowserRouter 来说它使用的是 React 为它封装的 history API ,这里的 history 和浏览器中的 history 有所不同噢!通过操作这些 API 来实现路由的保存等操作,但是这些 API 是 H5 中提出的,因此不兼容 IE9 以下版本。
对于 HashRouter 而言,它实现的原理是通过 URL 的哈希值,但是这句话我不是很理解,用一个简单的解释就是
我们可以理解为是锚点跳转,因为锚点跳转会保存历史记录,从而让 HashRouter 有了相关的前进后退操作,HashRouter 不会将 # 符号后面的内容请求。兼容性更好!
地址栏的表现形式不一样
- HashRouter 的路径中包含 # ,例如 localhost:3000/#/demo/test
刷新后路由 state 参数改变
- 在BrowserRouter 中,state 保存在history 对象中,刷新不会丢失
- HashRouter 则刷新会丢失 state