导航守卫
我们知道,Vue
给我们提供了几个钩子函数来让我们完成导航守卫的功能,全局的有 beforeEach
和 afterEach
,组件内部的钩子函数有 beforeRouteEnter
,beforeRouteUpdate
和 beforeRouteLeave
,因而我们可以在路由跳转之前或之后做一些类似 权限验证
或者 更改标签页标题
等相关操作
而 React Router
仅仅给我们提供了几个路由跳转的组件以及显示内容的组件,却并未给我们提供这么一些钩子函数,所以,这个方案就需要我们自己来实现了
我们需要封装一个路由守卫组件,通过 withRouter
或 useHistory
获取 history 对象
history 对象
有一个listen
函数:它会添加一个监听器,监听地址的变化,当地址发生变化时,会调用给它传递的这个函数
- 这个函数的运行时间点为:
即将跳转到新页面之前
- 传递的函数接受两个参数:
location 对象
和跳转方式('POP' / 'PUSH' / 'REPLACE')
- 调用
listen
函数的返回值是一个取消监听路由变化的函数
跳转方式 (action
):
-
'POP'
:出栈;(通过点击浏览器前进/后退
,调用history.go()
/history.goBack()
/history.goForward()
跳转) -
'PUSH'
:入栈;(调用history.push()
跳转) -
'REPLACE'
:入栈;(调用history.replace()
跳转)
直接动手 ✅
import { PureComponent } from 'react'
import { withRouter } from 'react-router-dom'
class GuardRouter extends PureComponent {
componentDidMount() {
this.unListen = this.props.history.listen((location, action) => {
console.log(location) // location 对象
console.log(action) // 'POP' / 'PUSH' / 'REPLACE'
const prevLocation = this.props.location
// 约定属性 onRouterChange 传递一个函数,当页面跳转时处理一些事情
this.props?.onRouterChange?.(prevLocation, curlocation, action, this.unListen)
})
}
componentWillUnmount() {
this.unListen() // 取消路由变化的监听
}
render() {
return this.props.children
}
}
export default withRouter(GuardRouter)
// 使用示例:
import React from 'react'
import { BrowserRouter as Router } from 'react-router-dom'
import GuardRouter from './components/GuardRouter'
import Home from './views/Home'
import News from './views/News'
import AboutUs from './views/AboutUs'
let count = 0
function routerChange(prevLocation, curLocation, action, unListen) {
count++
console.log(
`日志${count}:${prevLocation.pathname} → ${curLocation.pathname},${action} 方式`
)
if (count === 5) unListen()
}
const App = () => {
return (
<Router>
<Link to='/'>首页</Link>
<Link to='/news'>新闻页</Link>
<Link to='/aboutus'>关于我们</Link>
<GuardRouter onRouterChange={routerChange}>
<Route path='/' component={Home} />
<Route path='/news' component={News} />
<Route path='/aboutus' component={AboutUs} />
</GuardRouter>
</Router>
)
}
export default App
到这里,我们已经能够监听到路由跳转的变化,而且也能够拿到路由跳转的相关信息,但是跟 Vue
的路由守卫还是差很多,因为我们还是不能阻塞它的路由跳转......
Block 住了
那么接下来,我们就需要设置 一个
阻塞,仅当 this.props.history.block
设置阻塞后 (如设置多个 block
会报警告,且只会传递最后一个设置的阻塞信息),Router
组件的 getUserConfirmation
属性传递的函数才会运行,且 block
设置的阻塞消息,会作为参数传递给 getUserConfirmation
函数的第一个参数,第二个参数是一个回调函数,用于决定是否跳转 (callback(true) / callback(false))
history.block
函数接受一个参数,可以是 一个字符串
,也可以是一个 返回字符串的函数
(每次跳转都会运行这个函数),这个返回字符串的函数可以接受两个参数,同 listen 函数
一样 (接受参数:location
和 action
)
// GuardRouter
class GuardRouter extends PureComponent {
componentDidMount() {
this.unListen = this.props.history.listen((location, action) => {
// add listen...
}
// 设置阻塞
this.props.history.block('设置阻塞的信息:确定跳转页面吗?')
}
componentWillUnmount() {
this.unListen()
}
render() {
return this.props.children
}
}
export default withRouter(GuardRouter)
// App
const App = () => {
// 切换路由触发
const getConfirm = useCallback((msg, callback) => {
console.log(msg) // 打印:'设置阻塞的信息:确定跳转页面吗?'
// callback(true) // 跳转
// callback(false) // 不跳转
}, [])
return (
<Router getUserConfirmation={getConfirm}>
{/* Link 及 Route 配置 */}
</Router>
)
}
export default App
拦截器级别提升
从上面的例子中,我们可以看到,设置阻塞(block
)与拦截(getUserConfirmation
)是分离的,很明显在封装使用上是不够合理的,所以我们就需要把我们封装的 GuardRouter
的级别往上提,用来替换 Router
根组件:
import { BrowserRouter as Router } from 'react-router-dom'
/* 辅助组件,不做显示,仅用于设置阻塞 */
class _GuardRouterHelper extends PureComponent {
componentDidMount() {
// 设置阻塞
this.props.history.block((location, action) => {
// 可以动态获取拦截消息
// 可拿到即将跳转的 location 对象和跳转方式
return ''
})
}
render() {
return null
}
}
const GuardRouterHelper = withRouter(_GuardRouterHelper)
// 此组件不处于 Router 的上下文中,拿不到 history 对象,则无法设置阻塞
class GuardRouter extends PureComponent {
handleRouterConfirm = (msg, next) => {
// 约定在路由跳转之前的处理函数 onBeforeEach,跳转权交给外层调用者
this.props.onBeforeEach &&
this.props.onBeforeEach(next)
}
render() {
return (
<Router getUserConfirmation={this.handleRouterConfirm}>
<GuardRouterHelper />
{this.props.children}
</Router>
)
}
}
export default GuardRouter // 直接导出
使用示例
import React from 'react'
import { BrowserRouter as Router } from 'react-router-dom'
import GuardRouter from './components/GuardRouter'
import Home from './views/Home'
import News from './views/News'
import AboutUs from './views/AboutUs'
function beforeRouterChange(next) {
if (/* 满足跳转条件 */) {
next(true) // 跳转路由
} else {
next(false) // 不跳转路由
}
}
export default function App() {
return (
<GuardRouter onBeforeEach={beforeRouterChange}>
<Link to='/'>首页</Link>
<Link to='/news'>新闻页</Link>
<Link to='/aboutus'>关于我们</Link>
<div className='page-container'>
<Route path='/' component={Home} />
<Route path='/news' component={News} />
<Route path='/aboutus' component={AboutUs} />
</div>
</GuardRouter>
)
}
再做提升
由上面所说的设置 block
,传递的参数可以是一个函数,这个函数可以拿到 location 对象
和 action
,所以我们可以把这些信息传递给 onBeforeEach
函数 (即跳转前的 location对象
,将要调往路径的 location 对象,跳转方式及跳转控制的方法),那么可以有以下 两种方案 :
-
使用全局变量接收
prevLocation
,location
,action
和unBlock
(因为 路由中history 对象
是共用的一个对象,设置阻塞也只有一个,所以用全局变量存储没有问题 ):import {BrowserRouter as Router} from 'react-router-dom' let prevLocation, curLocation, curAction, unBlock class _GuardRouterHelper extends PureComponent { componentDidMount() { // 设置阻塞 ===============================================> unBlock = this.props.history.block((location, action) => { prevLocation = this.props.location curLocation = location curAction = action return '' }) // 设置阻塞 ===============================================> } render() { return null } } const GuardRouterHelper = withRouter(_GuardRouterHelper) class GuardRouter extends PureComponent { /* 依次将全局变量传递出去 ======================================> */ handleRouterConfirm = (msg, next) => { this.props?.onBeforeEach?.(prevLocation, curLocation, curAction, next) } /* 依次将全局变量传递出去 ======================================> */ render() { return ( <Router getUserConfirmation={this.handleRouterConfirm}> <GuardRouterHelper /> {this.props.children} </Router> ) } } export default GuardRouter // 直接导出
-
这几个变量无特殊变量,则可借助
JSON.stringify
函数格式化为json
字符串返回,通过监听函数的msg
变量接收:import {BrowserRouter as Router} from 'react-router-dom' class _GuardRouterHelper extends PureComponent { componentDidMount() { // 设置阻塞 ===============================================> this.props.history.block((location, action) => { const prevLocation = this.props.location // 返回值会作为 listen 函数的第一个参数 msg return JSON.stringify({ prevLocation, location, action }) }) // 设置阻塞 ===============================================> } render() { return null } } const GuardRouterHelper = withRouter(_GuardRouterHelper) class GuardRouter extends PureComponent { /* 借助 JSON.parse 解析 json 字符串 ======================================> */ handleRouterConfirm = (msg, next) => { const { prevLocation, location, action } = JSON.parse(msg) this.props?.onBeforeEach?.(prevLocation, location, action, next) } /* 借助 JSON.parse 解析 json 字符串 ======================================> */ render() { return ( <Router getUserConfirmation={this.handleRouterConfirm}> <GuardRouterHelper /> {this.props.children} </Router> ) } } export default GuardRouter // 直接导出
取消阻塞器
和添加路由监听一样,history.block
会返回一个取消阻塞的函数,若要将这个取消阻塞的函数传递给 onBeforeEach
函数,就只能使用全局变量来存储,再作为参数传递了
class _GuardRouterHelper extends PureComponent {
componentDidMount() {
this.unBlock = this.props.history.block((location, action) => {
// 添加阻塞
})
}
componentWillUnmount() {
this.unBlock() // 取消阻塞
}
render() {
return null
}
}
const GuardRouterHelper = withRouter(_GuardRouterHelper)
最终代码
前面的监听器结合阻塞器一同完成路由拦截:
import { BrowserRouter as Router } from 'react-router-dom'
let unBlock
class _GuardRouterHelper extends PureComponent {
componentDidMount() {
unBlock = this.props.history.block((location, action) => {
const prevLocation = this.props.location
return JSON.stringify({
prevLocation,
location,
action
})
})
this.unListen = this.props.history.listen((location, action) => {
const prevLocation = this.props.location
// 约定属性 onRouterChange 传递一个函数,当页面跳转时处理一些事情
this.props?.onRouterChange?.(prevLocation, curlocation, action, this.unListen)
})
}
componentWillUnmount() {
ubBlock() // 取消阻塞
this.unListen() // 取消路由变化的监听
}
render() {
return null
}
}
const GuardRouterHelper = withRouter(_GuardRouterHelper)
class GuardRouter extends PureComponent {
handleRouterConfirm = (msg, next) => {
const {
prevLocation,
location,
action
} = JSON.parse(msg)
if (this.props.onBeforeEach && typeof this.props.onBeforeEach === 'function') {
this.props.onBeforeEach?.(prevLocation, location, action, next, unBlock)
} else {
next(true) // 默认跳转
}
}
render() {
return (
<Router getUserConfirmation={this.handleRouterConfirm}>
<GuardRouterHelper onRouterChange={this.props.onRouterChange} />
{this.props.children}
</Router>
)
}
}
export default GuardRouter
// 使用
// 决定路由是否跳转
function beforeRouterChange(prevLocation, curLocation, action, next, unBlock) {
if (/* 满足跳转条件 */) {
next(true) // 跳转路由
} else {
next(false) // 不跳转路由
}
// unBlock() // 取消阻塞
}
// 路由变化运行的函数
function handleRouterChange(prevLocation, curlocation, action, unListen) {
// unListen() // 取消路由跳转监听
}
export default function App() {
return (
<GuardRouter
onBeforeEach={beforeRouterChange}
onRouterChange={handleRouterChange}
>
<Link to='/'>首页</Link>
<Link to='/news'>新闻页</Link>
<Link to='/aboutus'>关于我们</Link>
<div className='page-container'>
<Route path='/' component={Home} />
<Route path='/news' component={News} />
<Route path='/aboutus' component={AboutUs} />
</div>
</GuardRouter>
)
}