一、vue router里控制滚动行为的核心是什么?
做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的beforeRouteEnter
或activated
钩子恢复:
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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。