啥是 router ?
router 是路由啊! 路由又是啥? 路由器? 在前端领域, 路由是用来保持UI界面与Url地址映射一致的工具。
Router 是 react-router 中的一个重要组件。所有的路由路径与组件的映射规则都应该被放在 Router 里面。
Route
说 Router 之前, 先说说 Route。
Route 是用于声明路由路径
与组件
之间映射关系
的组件
。
对,没错, Route 是一个高阶组件
, 但是它本身不做任何展示, 渲染的是传入的 component 组件。
使用 Route 的时候一般是这样的:
传入的两个属性(path,component)确定了一个组件和一个路径的映射规则。
Router 将会根据这个规则,监听页面url的变化,每一次变化进行一次上述规则的匹配,path 匹配成功则展示相应的 component。
就这么回事儿。
如果不明确在 Route 标签上标注 exact,匹配规则将会是模糊匹配
例如有两个路由:
/a
,/a/b
分别对应了A,B两个组件。如果此时路由的地址是/a/b
, 本来你是只想显示B,但是A也会跟着跳出来。如果给 Route 组件里面写了 React 元素 ,path属性将失去作用,component自然也失效,React 元素一定会被渲染出来。
一定会渲染
- 既然能传递 React 元素,那自然也可以是一个函数返回一个React元素咯。
{() => 一定会渲染
}
Router
- Router 会生成一个路由上下文。这个上下文对象里面有(history,location, match)等对象,Router 一直监听着路由的变化,每一次变化的详细信息就存放在这个对象里面。
- Router 一般会把整个页面包裹起来,因为它要为全局的提供上述上下文。
- Router 相关的子组件能够拿到这个上下文对象。某个子组件如果匹配上路径, 在这个组件的props里面, 就会有上下文里面的对象。
还是这行熟悉的代码(一个例子用一年系列)。
下面就来慢慢看:
history
-
action
: 表示路由是以什么样的方式改变的。只有三种,POP
,PUSH
,REPLACE
。是否想起了栈?浏览器的历史记录栈啊。 -
listen
:监听页面路径变化的函数。 -
location
:下面说。 -
push
:代码跳转路由方法。参数可选。
this.props.history.push('/home', {a: 1, b: 'abc'})
// 路由跳转理所当然可以传递参数
// 在Home里面, 使用 this.props.history.location.state 获取
// 注意上下文中有一个 location 对象, 二者是一样的 this.props.location.state。但是与 window.location 不一样。
-
replace
:同push
。区别是 replace 将当前页面地址替换为一个新的地址(当前地址在栈顶,替换掉栈顶的地址)。push 是将一个新的地址入栈。浏览器表现上看就是 replace 之后, 不能回退到之前的页面, push可以。
location
顾名思义,定位。当前页面的地址信息。与history中的location是同一东西。
-
pathname
: 当前页面匹配上的路径名。 -
search
: url 携带的 ?后面的参数。 -
hash
: url 携带的 # 后面的参数。 -
state
: 使用 push, replace 方法传递的参数。
可以使用query-string
这个第三方库解析search
,hash
import qs from 'query-string';
...
let query = qs.parse(this.props.location.search)
let hash = qs.parse(this.props.location.hash)
...
match
当前路由的匹配信息
-
isExact
: 不是上面说的 Route 上的 exact。它表示的是当前这个路由是否是由精确匹配匹配上的。(事实上的精确匹配) -
params
: 动态路由上携带的参数
// 将参数写入路径中,以 news/2020/04/21 的方式传递。
// 然后 react-router 使用类似正则的方式获取数据。
// 正则的规则(string-pattern)定义在path中
在News 组件中, 看看props:
注意到 params 了嘛?
其实格式不是固定的
这样也是可以的(尽情开脑洞吧), 只是一个约定而已, 怎么约定就怎么传。
:year?
:冒号后面表示变量, 如果再加上一个问号, 表明这个参数是可选的。 即使规定要传,实际没有传,不会报错,但是匹配会失败。
:day(\d+)
: 对字段 day 进行正则约束,需要是一个数字, 如果不是数字, 匹配会失败。
/*
: 必须要传点东西, 任意东西都行, 就是不能不传。
react-router
中依赖了path-to-RegExp
这个第三方库来完成上述匹配。
-
path
: 当前页面是用哪一个规则匹配上的。 -
url
: 当前页面被真实匹配上的路径。
Switch
Switch 也是 react-router 中的一个组件, 它提供一个容器, 里面放很多的 Route,当路由发生变化的时候,匹配这些 Route 。但是如果匹配上多个路由(模糊匹配,或者多个同路径Route),只有匹配上的第一个会生效。
原理:当浏览器url发生改变, 循环所有的子组件(Route),匹配子组件的path属性,渲染第一匹配上的 Route 的 component。所以,当Switch 里面如果不是 Route,报错就理所应当了吧。
跨组件的路由信息 - withRouter
现在有两个页面,第一个是 home, 第二个是news。
在home 里面嵌套了另外一个组件A, 我希望在A中使用一个按钮去看新闻。
export default function Home(props) {
console.log(props)
return (
)
}
function A (props) {
return
}
这样跳转肯定会出错。因为我们知道在React中, 组件的props属性全是来自父组件的传递: 。可是A啥都没有。
于是:
function Home(props){
return {
...
...
}
}
Home 组件的 props 里面是含有路由信息的,
所以可以考虑直接将Home组件的props也给A组件,
但是如果组件嵌套层级比较深的话,那就太快乐了。
使用 withRouter:
import {withRouter} from 'react-router-dom';
A = withRouter(A)
// 使用A
withRouter 原理:
withRouter = Component =>
A 组件不是不能拿到路由信息嘛?为什么不能拿到? 因为A组件没有被放在 Route 的 component 里面。那我给你放进去就完了。
Link
原生 dom 里面的a标签跳转地址是需要刷新页面的, 不信去试试吧。
Link 用于生成一个无刷新跳转页面的 a 元素。
class Link extends Component {
render() {
return (
{
e.preventDefault();
this.props.history.push(this.props.to)
}}
>{this.props.children}
)
}
}
export default withRouter(Link)
// 别忘了withRouter 包一下,否则...
Link:
-
to
要跳转的地址
// 使用对象的方式跳转
Home
// 直接使用字符串跳转
Home
// 可以传递 ref
console.log(ref)}>Home
// 使用 replace 的方式跳转, 默认为 push
Home
NavLink
Link 的升级版。
浏览器的地址,NavLink 实体a元素的 href 属性,这两个东西对应上的时候,会给a标签加上一个 active 属性。表示a被点击激活了。
// 自定义 active 类名
Home
Redirect
跳转到指定页面。默认使用 replace 的方式跳转。
-
from
: 如果匹配到 from 的地址, 进行重定向到 to 的地址。
嵌套的路由
function A (){
return (
这是一条新闻~
)
}
function B (){
return (
这是一条新闻评论~
)
}
export default function Page(props) {
return (
看新闻
看新闻
)
}
在根组件中套了一层Router,可以在任意页面写 Route.
这个东西写在什么地方,只要path属性匹配上了,就在对应的位置渲染component。嵌套路由就是这么实现的。
受保护的路由
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
export default function ProtectRoute({ component: Component, children, render, ...rest }) {
return (
{
if (isLogin) { // 鉴权逻辑, 此处为示例
return
} else {
return
}
}}
>
)
}
// use
这里返回了一个Route组件, 前面没有提到的是, Route 组件可以传递一个render 属性, 这个属性接收一个函数, 函数默认参数是路由上下文对象。在 ProtectRoute 的参数里, 解构出了Component, children,render, 因为这三个参数会影响Route的表现行为, 这种表现行为不是此处场景需要的(它们会直接渲染,就没办法处理业务逻辑)。其他的参数通过对象展开运算符收纳在rest中,rest中的参数是可以直接作为参数再传递给Route的。
如果鉴权失败,返回一个重定向的路由去登录页, 同时携带上当前访问被拦截的pathname,等登陆完成之后, 可以通过这个pathname再跳转回来。简化用户操作。
导航守卫
vue
的 beforeRouteEnter
香不香?那肯定香的呀,放心,react 里面也....没有...但是.....可以自己实现。
history 对象 中有一个 listen 函数, 这个之前只是一提,没有深入细说, 就在这里等着呢。
函数默认接收两个参数:
- location 当前路由的定位信息
- action 当前监听到的路由变化以何种方式触发。POP,PUSH,REPLACE。
class RouteGuard extends Component {
componentDidMount(){
this.props.history.listen((location, action) => {
console.log(action, location.pathname)
})
}
render() {
return (
{this.props.children}
)
}
}
export default withRouter(RouteGuard)
function App() {
return (
);
}
每一次路由的变化都会被history.listen监听到。当然, RouteGuard 的位置也很关键。
此时还没有什么实际用处, 仅仅是能够看到路由跳转确实经过了这里,这个卡还没设起来。
function App() {
return (
{
// do something
callback(true)
}}
>
{
// do something
}}
>
);
}
class RouteGuard extends Component {
componentDidMount(){
this.props.history.listen((location, action) => {
console.log(action, location.pathname)
this.props.onUrlChange && this.props.onUrlChange(location, action)
})
this.props.history.block('真的要跳转吗?')
}
render() {
return (
{this.props.children}
)
}
}
在
Router
下的任意能接收到路由信息的组件中, 设置阻塞(此阻塞只能设置一个)(history.block('msg')
)之后,才能真正实现守卫的功能,在Router
中有一个属性:getUserConfirmation
,这个属性需要配置一个函数,函数接收两个参数,msg
和callback
。msg
就是阻塞处传递的字符串,callback
则决定了此次路由跳转请求是否跳转。在getUserConfirmation
函数中进行业务逻辑处理(比如鉴权)之后,使用callback(true or false)
。
现在, 写了一个守卫组件, 组件中将当前路由跳转信息抛出, 在Router 中去处理守卫逻辑。当路径发生改变的时候, 执行onUrlChange
函数,比如打个日志啥的。然后会在 Router
中的 getUserConfirmation
看是否允许此次跳转。但是这样好像有点不满足,有点麻烦。
- 在增强
RouteGuard
之前, 这里补充两个概念-
history.listen
: 用来监听路由变化, 每次变化都会触发这个函数。 -
history.block
:每次路由变化设置阻塞, 需要通过getUserConfirmation
决定是否放行。
-
增强 RouteGuard
import React, { Component } from 'react'
import { BrowserRouter as Router, withRouter } from 'react-router-dom'
let prevLocation, nextLocation, action, unBlock;
class RouteGuard extends Component {
handleComfirm = (msg, callback) => {
this.props.onUrlChange ?
this.props.onUrlChange(prevLocation, nextLocation, action, unBlock, callback) : callback(true)
}
render() {
return (
{this.props.children}
)
}
}
class GuardHelper extends Component {
componentDidMount() {
this.unListen = this.props.history.listen((location, action) => {
console.log(action, location.pathname, this.unListen)
})
unBlock = this.props.history.block((location, ac) => {
prevLocation = this.props.location
nextLocation = location
action = ac
return ''
})
}
render() {
return null
}
}
GuardHelper = withRouter(GuardHelper)
export default RouteGuard
首先, RouteGuard 返回的是一个 Router 组件, 依然只能是在 Router中处理这个事件, 但是我们将这个事件交给 RouteGuard 上传递进来的自定义事件 onUrlChange
。
这个自定义事件我们希望给他传递prevLocation(由那个页面跳转的页面信息), nextLocation(要去哪个页面的页面信息), action(以何种方式跳转), unBlock(取消阻塞), callback(允许跳转与否)
这五个参数。所以这里要考虑一个问题:RouteGuard
现在是根组件,路由信息从哪里来?
我们知道只有 Router
下的 Route
中 component
会默认得到路由信息。所以这里创建了一个 新的组件 : GuardHelper
。将这个组件使用 withRouter
包装一下, 这样他就能够接收路由信息,但是这还不够, 还没有人传递给它,我们还需要把它放在 Router
下。
这样, 就可以顺利在 GuardHelper
中拿到路由信息。但是处理阻塞的 handleConfirm
函数在根组件 RouteGuard
里面,是这个函数需要路由信息。于是我们创建了几个全局变量, 不用担心,这个全局变量只在这个模块里面生效。GuardHelper
这个组件 componentDidMount
的时候,调用 history.listen
监听路由变化,history.block
设置阻塞,并且将路由信息保存在全局变量中, 这样RouteGuard 就能拿到了。
注意:(history.block 和 getUserConfirmation)
必须是成对出现,依靠设置的阻塞, 才能通过 getUserConfirmation
处理这个阻塞。
// 现在这样使用
function App() {
return (
{
// do something
console.log(prevLocation.pathname, nextLocation.pathname, action)
commit(false)
}}
>
);
}
(不作为教程,前端知识冗杂,仅供自己备忘)