React学习之实现React Router导航守卫【React React Router】

2,312 阅读6分钟

React学习之实现React Router导航守卫

导航守卫

我们知道,Vue 给我们提供了几个钩子函数来让我们完成导航守卫的功能,全局的有 beforeEach和 afterEach,组件内部的钩子函数有 beforeRouteEnterbeforeRouteUpdate 和 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 &&
                this.props.onRouterChange(prevLocation, location, 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()
}

export default function 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>)
}

到这里,我们已经能够监听到路由跳转的变化,而且也能够拿到路由跳转的相关信息,但是跟 Vue的路由守卫还是差很多,因为我们还是不能阻塞它的路由跳转......

那么接下来,我们就需要设置 一个 阻塞,仅当 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
export default function App() {
    // 切换路由触发
    const getConfirm = useCallback((msg, callback) => {
        console.log(msg) // 打印:'设置阻塞的信息:确定跳转页面吗?'
        // callback(true) // 跳转
        // callback(false) // 不跳转
    }, [])

    return (<Router getUserConfirmation={getConfirm}>
        {/* Link 及 Route 配置 */}
    </Router>)
}

拦截器级别提升

从上面的例子中,我们可以看到,设置阻塞(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>
            <Route path='/' component={Home} />
            <Route path='/news' component={News} />
            <Route path='/aboutus' component={AboutUs} />
        </div>
    </GuardRouter>)
}

再次提升

由上面所说的设置 block,传递的参数可以是一个函数,这个函数可以拿到 location 对象 和 action,所以我们可以把这些信息传递给 onBeforeEach 函数 (即跳转前的 location对象,将要调往路径的 location 对象,跳转方式及跳转控制的方法),那么可以有以下两种方案:

  1. 使用全局变量接收 prevLocationlocationaction 和 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 &&
                this.props.onBeforeEach(prevLocation, curLocation, curAction, next)
        }
        /* 依次将全局变量传递出去 ======================================> */
        render() {
            return <Router getUserConfirmation={this.handleRouterConfirm}>
                <GuardRouterHelper />
                {this.props.children}
            </Router>
        }
    }
    export default GuardRouter // 直接导出
    
  2. 这几个变量无特殊变量,则可借助 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 &&
                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 &&
                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) {
            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>
                <Route path='/' component={Home} />
                <Route path='/news' component={News} />
                <Route path='/aboutus' component={AboutUs} />
            </div>
        </GuardRouter>
    )
}