Code前端首页关于Code前端联系我们

一、vue router里控制滚动行为的核心是什么?

terry 2天前 阅读数 18 #Vue

做Vue单页应用时,路由切换后页面滚动位置乱掉、前进后退不记得之前滚到哪…这些情况是不是让你头疼?其实vue - router里的scroll配置能解决这些问题,但不少同学配置时总踩坑,今天就用问答形式,把vue - router scroll的核心逻辑、场景处理、避坑技巧一次性讲清楚~

vue - router专门提供了scrollBehavior函数来管理路由切换时的滚动位置,它是路由配置(router/index.js)里的一个选项,作用是:当路由切换完成后,决定页面(或滚动容器)该滚动到哪里。

看个基础结构:

const router = new VueRouter({
  mode: 'history',
  routes: [...],
  scrollBehavior(to, from, savedPosition) {
    // 返回滚动位置对象,{ x: 0, y: 0 }
  }
})

这里有三个参数要理解:

  • to:目标路由的信息对象(要跳转到哪);
  • from:当前离开的路由信息对象(从哪跳过来);
  • savedPosition:只有用户点击浏览器“前进/后退”按钮时才会有值,它记录了之前页面的滚动位置(类似浏览器原生的历史记录滚动恢复)。

想让路由切换后页面“回到顶部”,怎么配?

最常见的需求——每次切路由,页面自动滚到顶部,这时候给scrollBehavior返回固定的滚动坐标就行:

scrollBehavior() {
  return { x: 0, y: 0 }
}

但有个细节要注意:路由模式得是history,如果用hash模式(url带#),savedPosition大概率拿不到有效值,因为hash变化时浏览器不会像history模式那样精准记录滚动位置。

要是做移动端项目,页面顶部有固定导航栏(比如44px高),想让内容从导航栏下方开始显示,就把y值改成导航栏高度:

scrollBehavior() {
  return { x: 0, y: 44 } // 假设导航栏高度44px
}

用户前进后退时,怎么让页面“之前的滚动位置?

很多场景需要「前进后退恢复滚动」,比如从列表页划到第20条,点进详情页再返回,列表页得回到第20条的位置,这时候savedPosition就派上用场了:

scrollBehavior(to, from, savedPosition) {
  if (savedPosition) {
    // 有保存的位置,就回到那里
    return savedPosition
  } else {
    // 没有就回到顶部
    return { x: 0, y: 0 }
  }
}

但要注意:只有history模式下,浏览器才会在用户点击前进/后退时,把滚动位置存在savedPosition里,如果是hash模式,这个功能基本失效(因为hash变化不会触发浏览器历史记录的滚动快照逻辑)。

嵌套路由场景下,滚动行为怎么处理?

嵌套路由(比如父路由里套子路由)很容易出滚动问题,举个例子:父组件有个带滚动条的区域(比如<div class="parent - scroll" ref="parentScroll">),子路由切换时,想让父组件的滚动区域回到顶部,而不是整个页面滚动。

这时候scrollBehavior就不够用了——因为它默认控制的是整个窗口(window)的滚动,而不是某个div的滚动,这时候得结合「路由钩子」或「组件内监听路由」来手动处理:

方法1:用路由钩子(beforeRouteEnter)

在子组件里写:

export default {
  beforeRouteEnter(to, from, next) {
    next(vm => {
      // vm是当前组件实例,找到父组件的滚动容器
      const parentScroll = vm.$parent.$refs.parentScroll
      parentScroll.scrollTop = 0
    })
  }
}

方法2:在父组件里监听路由变化

父组件中:

export default {
  watch: {
    $route() {
      this.$refs.parentScroll.scrollTop = 0
    }
  }
}

这种场景的核心是:如果滚动容器不是window,scrollBehavior管不着,必须手动操作DOM

动态路由(带参数)切换时,怎么保留滚动位置?

动态路由比如/product/:id,从/product/1切到/product/2,再切回来,这时候路由属于“同一路由(name相同,参数不同)”,savedPosition不会生效(因为不是浏览器前进后退触发的)。

这时候得自己“存”和“取”滚动位置,推荐用路由元信息(meta)或者状态管理(Vuex/Pinia)

步骤1:在路由离开时存滚动位置

在列表页(假设是ProductList组件)的beforeRouteLeave钩子存滚动位置:

export default {
  data() {
    return {
      scrollTop: 0
    }
  },
  beforeRouteLeave(to, from, next) {
    // 假设滚动容器是window,存window.scrollY
    this.scrollTop = window.scrollY
    next()
  }
}

步骤2:在路由进入时恢复滚动位置

同样在ProductList的beforeRouteEnteractivated钩子恢复:

export default {
  activated() {
    // 组件被激活时(如果用了keep - alive),恢复滚动
    window.scrollTo(0, this.scrollTop)
  },
  beforeRouteEnter(to, from, next) {
    next(vm => {
      window.scrollTo(0, vm.scrollTop)
    })
  }
}

如果滚动容器是某个div,就把window.scrollY换成div.scrollTop,逻辑一样。

用了keep - alive缓存组件,scroll怎么适配?

keep - alive会缓存组件实例,路由切换时组件不会销毁,所以scrollBehavior可能“失效”——因为组件自己的滚动状态被缓存了,不会触发路由级别的滚动配置。

这时候得在组件的生命周期钩子里处理:

场景:列表页用keep - alive缓存,切路由后返回要保留滚动位置

在列表组件里:

export default {
  data() {
    return {
      savedScroll: 0
    }
  },
  // 组件被缓存前,存滚动位置
  deactivated() {
    this.savedScroll = window.scrollY // 或div的scrollTop
  },
  // 组件被激活时,恢复滚动位置
  activated() {
    window.scrollTo(0, this.savedScroll)
  }
}

如果是多个keep - alive组件,每个组件都要单独存自己的滚动位置,避免互相影响。

移动端“滚动穿透”和scroll配置冲突咋解决?

移动端做弹窗(比如Modal)时,经常出现“弹窗后面的页面还能滚动”(滚动穿透),很多同学会给body加overflow: hidden来禁止滚动,但路由切换后忘记恢复,导致页面不能滚动了…

可以结合路由元信息(meta)全局路由守卫来控制:

步骤1:给需要禁止滚动的路由加meta标记

const routes = [
  {
    path: '/modal - page',
    component: ModalPage,
    meta: {
      disableScroll: true // 标记这个路由需要禁止滚动
    }
  }
]

步骤2:在全局路由守卫里处理body样式

router.beforeEach((to, from, next) => {
  if (to.meta.disableScroll) {
    document.body.style.overflow = 'hidden'
  } else {
    document.body.style.overflow = 'auto'
  }
  next()
})

但这样处理后,路由切换时scrollBehavior设置的滚动位置可能因为body样式变化失效,所以要在scrollBehavior里延迟设置滚动,或者用setTimeout兜底:

scrollBehavior() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ x: 0, y: 0 })
    }, 100)
  })
}

(原理:等body样式恢复后,再设置滚动位置,避免样式冲突导致滚动不生效。)

用第三方组件库的滚动组件,怎么和vue - router scroll配合?

比如用Element UI的<el - scrollbar>,它的滚动是基于内部的.el - scrollbar__wrap元素,这时候scrollBehavior控制window滚动没用,得手动操作这个内部元素。

以ElScrollbar为例,步骤如下:

给ElScrollbar加ref

<el - scrollbar ref="elScroll">
  <!-- 列表内容 -->
</el - scrollbar>

在路由切换时,找到滚动容器并设置scrollTop

可以在组件的watch.$route里处理:

export default {
  watch: {
    $route() {
      // ElScrollbar的滚动容器是 .el - scrollbar__wrap
      const scrollWrap = this.$refs.elScroll.$refs.wrap
      scrollWrap.scrollTop = 0 // 切路由后回到顶部
    }
  }
}

不同组件库的滚动容器结构不同,比如Ant Design Vue的<a - scrollbar>,要找到它的滚动容器类名(比如.ant - scrollbar - wrap),原理一样:先拿到组件实例,再找到内部滚动DOM,手动设置scrollTop。

scrollBehavior配置后不生效,常见原因有哪些?

遇到“配置了scrollBehavior但没效果”,先排查这几个点:

路由模式不是history

如果用hash模式(url带#),savedPosition基本拿不到有效值,而且浏览器对hash路由的滚动记录支持不好,换成history模式试试(记得后端配置路由重定向,避免404)。

滚动容器不是window

如果页面滚动是某个div的overflow: auto实现的,scrollBehavior控制的是window滚动,对div无效,这时候必须手动操作div的scrollTop(参考嵌套路由和第三方组件库的处理方法)。

异步组件加载时机问题

如果路由用了异步组件(比如component: () => import('./views/xxx.vue')),scrollBehavior执行时,组件可能还没渲染到页面,导致滚动位置设置无效,可以给scrollBehavior加个延迟:

scrollBehavior() {
  return new Promise((resolve) => {
    this.$nextTick(() => {
      resolve({ x: 0, y: 0 })
    })
  })
}

(原理:等组件渲染完成后,再设置滚动位置。)

CSS样式冲突

比如给body或html加了overflow: hidden,导致window无法滚动,scrollBehavior的y:0自然没效果,检查全局样式,把不必要的overflow隐藏去掉。

scroll配置的性能优化要注意什么?

如果页面有长列表、大量图片,频繁切换路由时每次都设置滚动位置,可能造成性能问题(比如布局抖动、频繁重绘),可以做这些优化:

节流处理滚动设置

用lodash的_.throttle,或者自己写节流函数,避免短时间内多次设置滚动:

import { throttle } from 'lodash'
const setScroll = throttle(() => {
  window.scrollTo(0, 0)
}, 200)
scrollBehavior() {
  setScroll()
  return { x: 0, y: 0 }
}

用requestAnimationFrame优化

把滚动设置放到requestAnimationFrame里,让浏览器在帧渲染时处理,减少性能开销:

scrollBehavior() {
  return new Promise((resolve) => {
    requestAnimationFrame(() => {
      resolve({ x: 0, y: 0 })
    })
  })
}

只在关键路由生效

如果某些路由(比如首页、登录页)不需要复杂滚动逻辑,就跳过scrollBehavior处理:

scrollBehavior(to, from) {
  if (to.name === 'Home' || to.name === 'Login') {
    return { x: 0, y: 0 }
  }
  // 其他路由根据需求处理
  return savedPosition || { x: 0, y: 0 }
}

vue - router的scroll配置核心是`scrollBehavior`函数,但实际场景要考虑路由模式、滚动容器、组件缓存、移动端适配等细节,滚动容器是谁,就操作谁的scrollTop”,遇到问题先排查模式、DOM结构、样式冲突这几点,大部分坑都能绕开~如果还有特殊场景(比如多Tab页面、横向滚动),原理也是一样的:找到滚动容器,控制它的滚动位置就行~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门