config/history.js

/**
 * @class History
 * @classdesc 通过vue-router的onRouteChangeBefore事件构建本地历史记录
 *
 * ## 问题
 *
 * 单页应用的一个需求是需要知道路由切换是前进还是后退, 但是浏览器对路由切换只给了两个事件 `hashchange` 和 `popstate`, 故无从判断当前操作是后退还是前进.
 *
 * ## 解决方案
 *
 * 这个类通过vue-router的onRouteChangeBefore事件构建本地历史记录. 当路由切换时, 内建历史记录数组, 类似于一个stack, 这个能正确反映当前app的浏览历史记录.
 *
 * 完成的功能如下:
 *
 * - 内建导航记录
 * - 此History是对router实例的拓展, 但是不会为router实例添加方法, 而是重新定义$history, 这个可在业务的this中访问到
 */

export class History {
    constructor(router, config, platform) {
        this._history = [] // 存储当前导航的历史记录, 内容为 route object(路由信息对象)
        this._direction = 'forward' // forward/backward
        this._router = router // vue-router实例
        this._config = config // config 实例
        this._platform = platform // platform 实例
        this.isReplace = false // 路由跳转是否是使用replace方法
        this.usePushWindow = this._config.getBoolean('usePushWindow', false) // 支付宝 和 钉钉 两个模式下路由跳转是否开启新页面

        // 监听路由变化, 维护本地历史记录
        // 路由切换前
        if (this._router) {
            const _this = this

            /**
             * 方法增强: replace()
             * */
            let replaceCopy = this._router.replace
            replaceCopy = replaceCopy.bind(this._router)
            this._router.replace = function () {
                let args = Array.from(arguments)
                let to = _this._router.resolve(args[0]).resolved
                let from = _this._history[_this.length - 1]
                if (to.fullPath !== from.fullPath || to.name !== from.name) {
                    // replace时, 前后地址不一样的话才处理
                    _this.isReplace = true
                    replaceCopy.apply(null, arguments)
                }
            }

            if (this.usePushWindow) {
                /**
                 * 方法增强: back()
                 * */
                let backCopy = this._router.back
                backCopy = backCopy.bind(this._router)
                this._router.back = function () {
                    let isHandled =
                        _this._platform.popWindow && _this._platform.popWindow()
                    if (!isHandled) {
                        backCopy.apply(null, arguments)
                    }
                }

                /**
                 * 方法增强: go()
                 * */
                let goCopy = this._router.go
                goCopy = goCopy.bind(this._router)
                this._router.go = function (n) {
                    let isHandled = _this._platform.popTo && _this._platform.popTo(n)
                    if (!isHandled) {
                        goCopy.apply(null, arguments)
                    }
                }
            }

            this._router.beforeEach((to, from, next) => {
                // 如果使用了replace, 则跳过当前拦截的路由信息, 并且将最后一个重置
                if (this.isReplace) {
                    this.isReplace = false
                    this._history.pop()
                    this._history.push(to)
                    next()
                } else if (this.length <= 1) {
                    /**
                     * 当本地维护的历时记录为空或, 意味着页面为首次进入, 并未初始化,
                     * 此时, 可能我们是从app中的某个页面进入的,
                     * 因此, 需要判断下history.length, 此时, 不显示back按钮
                     *
                     * 同理, length=1也同样处理
                     * */
                    this._pushHistory({to, from, next})
                } else {
                    // 向记录后方追溯, 如果有匹配可认为是go(-n)操作, 否则就是push操作
                    for (let i = this.length - 2; i > -1; i--) {
                        let _previous = this._history[i]
                        if (to.name === _previous.name) {
                            this._popHistory(next, i)
                            return
                        }
                    }

                    // 如果不在过去的历史记录, 则认为是新增加记录
                    this._pushHistory({to, from, next})
                }
            })
        }
    }

    get length() {
        return this._history.length
    }

    // -------- private --------
    /**
     * push to history
     * @private
     * */
    _pushHistory({to, from, next}) {
        if (to.fullPath === from.fullPath && to.name === from.name) {
            // 同地址同名称跳转不记录不处理
            return
        }

        let isHandled = false
        if (
            this.usePushWindow &&
            from.matched.length !== 0 &&
            to.matched.length !== 0
        ) {
            let url = ''
            let mode = this._router.mode
            if (mode === 'hash') {
                url = `${window.location.origin}${window.location.pathname}${window
                    .location.search}#${to.fullPath}`
            } else {
                console.error('history.js::只支持 mode: "hash"')
            }
            isHandled = this._platform.pushWindow && this._platform.pushWindow(url)
        }

        if (!isHandled) {
            // fallback
            this._direction = 'forward'
            this._history.push(to)
            next()
        }
    }

    /**
     * pop history record
     * @private
     * */
    _popHistory(next, i = 0) {
        // 激活了浏览器的后退,这里只需要更新状态
        this._direction = 'backward'
        this._history = this._history.splice(0, i + 1)
        next()
    }

    /**
     * 获取当前的页面进行的方向
     * 只能是这两个值: forward || backward
     * @return {string}
     * */
    getDirection() {
        return this._direction
    }

    /**
     * 判断是否能返回
     * @return {Boolean}
     * */
    canGoBack() {
        return this.length > 1
    }

    /**
     * 获取当前的导航记录
     * @return {Array}
     * */
    getHistory() {
        return this._history
    }

    /**
     * 返回root页面(在路由信息中标示```route.meta.root=true```的页面)
     * @example
     * {
   *    path: '/',
   *    name: 'index',
   *    meta: {
   *      root: true
   *    },
   *    component: require('@/pages/index.vue')
   * }
     * */
    toRoot() {
        // 支付宝方式返回首页
        let isHandled = this._platform.popToRoot && this._platform.popToRoot()
        if (!isHandled) {
            if (this._router.options.routes) {
                let routes = this._router.options.routes
                for (let i = 0, len = routes.length; len > i; i++) {
                    let route = routes[i]
                    if (route && route.meta && route.meta.root) {
                        console.log(route)
                        this._direction = 'backward'
                        this._router.replace(route)
                        this._history = []
                        return
                    }
                }
            }

            if (!isHandled) {
                this._router.go(1 - this.length)
            }
        }
    }
}

export function setupHistory(router, config, platform) {
    return new History(router, config, platform)
}