你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
Mayo

hash模式下Vue-router页面返回锚点(scroll behavior)实现 0个回复 专栏 @ 框架与库

Mayo 发布于 2 月前

原文链接:http://meixg.cn/2017/05/24/vue-anchor-scroll/

在普通页面中,点击浏览器的返回按钮,在返回到上一页时会处在上次浏览的位置。单页面应用中,由于始终是同一个页面, 因此需要自行实现页面返回时的锚点。Vue-router的Scroll Behavior可以用于解决这个问题,但是只能应用在HTML5 history模式。本文实现了在hash模式下的锚点跳转。

锚点位置存储

Vue-router要求在HTML5 history模式下,是为了使用pushState、replaceState API以及popstate事件:

参考源码html5.js

// Vue-router中的push方法
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
    pushState(cleanPath(this.base + route.fullPath))// 存储页面锚点位置
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
}, onAbort)
}
// pushState方法
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}
export function setupScroll () {
    window.addEventListener('popstate', e => {
        saveScrollPosition();
        if (e.state && e.state.key) {
            setStateKey(e.state.key);
        }
    })
}

window.addEventListener('popstate', e => {
    this.transitionTo(getLocation(this.base), route => {
    if (expectScroll) {
        handleScroll(router, route, this.current, true)
    }
    })
})

在hash模式下需要我们自己记录锚点位置。可以维护一个与history相同的数组,每次页面跳转时在Vue-router提供的钩子函数中遍历数组,存储锚点位置。

锚点滚动

Vue-router本身提供了scrollBehavior方法,用来进行锚点跳转。但是该方法只能用在HTML5 history模式下。研究了一下其源码:vue-router/src/util/scroll.js,发现也是使用window.scrollTo()来进行页面的滚动。重要的是设置滚动的时机,应当在下一个页面绘制完成后进行跳转(wait until re-render finishes before scrolling)。

// wait until re-render finishes before scrolling
router.app.$nextTick(() => {
    let position = getScrollPosition();
    const shouldScroll = behavior(to, from, isPop ? position : null);
    if (!shouldScroll) {
        return;
    }
    const isObject = typeof shouldScroll === 'object';
    if (isObject && typeof shouldScroll.selector === 'string') {
        const el = document.querySelector(shouldScroll.selector);
        if (el) {
            position = getElementPosition(el);
        } else if (isValidPosition(shouldScroll)) {
            position = normalizePosition(shouldScroll);
        }
    }
    else if (isObject && isValidPosition(shouldScroll)) {
        position = normalizePosition(shouldScroll);
    }

    if (position) {
        window.scrollTo(position.x, position.y);
    }
})

我们目前已经自行记录了锚点,因此可以在router中模仿一个跳转过程:

router.beforeEach((to, from, next) => {
    next();
    // 重要的是设置跳转的时机。过早的话页面还没加载完成,高度不够导致不能滚动。过晚的话会造成页面的闪烁。
    router.app.$nextTick(() => { 

        // 获取history数组中的最后一个browserHistoryLast

        if (to.fullPath === browserHistoryLast.path
            && browserHistoryLast.pos) {
            document.body.scrollTop = browserHistoryLast.pos;
        }
        else {
            document.body.scrollTop = 0;
        }
    });
});

在router的钩子函数中,调用next之后,在$nextTick中进行页面的滚动,即可达到和scrollBehavior相似的效果。

需要注意的是,各个页面的数据应当存储在vuex中,不能每次进入页面都发送请求(即使不锚点也应当这么做)。否侧因为返回时页面还在请求数据,不能达到锚点的效果。

关于过场动画

如果页面跳转有过场动画存在,非常容易在锚点滚动时发生闪烁。尝试了几种方式,都没能达到很好效果。

尝试过的方法的思路在这里记录一下,这些方法都会有很大的抖动闪烁,根本原因还是页面跳转的时机不对:

  • 返回到有存储pos的旧页面时,在onTransitionAfterEnter中将页面滚动到记录的位置。打开新页面时,在onTransitionBeforeStart中将页面滚动设置为0,确保新页面在顶部。
  • vue-router的过渡动画使用的是absolute定位+transform。因此尝试了给页面设置top值来消除闪烁。在跳转前给当前页面设置与目标页面滚动位置相同的top值,在滚动结束后由于不再是absolute定位,top值不再生效,没有闪烁发生。在返回时,列表页会首先绝对定位到首页要滚动的位置(此时会有闪烁),之后直接跳转到首页。闪烁集中在返回过渡效果之前。

其他问题

  • 全局mixin中不能写组件中的过渡钩子,如beforeRouteEnter等,会报错。

    这个issue中说已经修复了mixins usage are fixed in fb32ccb, 但是还是用不了。

  • computed只有在vuex中的变量变化时,才会进行更新。import进来的值不行。

    在onTransitionBeforeStart修改变量,不会使from页面中的computed更新。如果想要在页面跳转时更新from页面的computed,需要在router的钩子函数中进行修改,在this.$nextTick中调用next()。

等待第一条回复
登录后回复,如无账号,请使用邀请码注册