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

Vue Router 怎么保持页面滚动位置?

terry 2小时前 阅读数 7 #Vue

在 Vue 开发单页面应用时,切换路由后页面自动滚回顶部是常见情况,但有时候我们希望保留滚动位置——比如列表页翻了好几页,返回时还能停在原来位置,那 Vue Router 怎么实现滚动位置保持?这篇文章从原理、方法到实战细节,一步步讲清楚。

先理解 Vue Router 的滚动默认行为

Vue Router 开发单页应用时,路由切换本质是组件销毁与重建(或切换),默认情况下,每次路由跳转后页面会自动滚动到顶部,这是框架为“新页面从顶部开始浏览”这类通用场景设计的体验逻辑。

但业务里总有特殊需求:从详情页返回列表页,保留列表之前的滚动位置”,这时候默认行为就成了阻碍——因为组件销毁后,滚动容器(window 或可滚动的 div)的 scrollTop 等状态会丢失,必须主动干预才能“位置。

实现滚动保持的核心思路:“保存”与“恢复”

要实现滚动位置保持,核心逻辑是 “离开页面时保存滚动位置,进入页面时恢复位置”,但不同场景(比如浏览器前进后退、编程式导航、组件是否缓存)需要不同的技术方案,下面逐个拆解。

(一)浏览器前进后退场景:用 scrollBehavior 配置

Vue Router 提供了 scrollBehavior 配置项,专门处理浏览器导航栏的“前进/后退”(即 popstate 事件)时的滚动行为。

router/index.js 中配置:

const router = createRouter({
  history: createWebHistory(),
  routes: [...],
  scrollBehavior(to, from, savedPosition) {
    // savedPosition:仅在浏览器前进/后退时有效,存储了原生滚动位置
    if (savedPosition) {
      return savedPosition; // 恢复之前的滚动位置
    } else {
      return { top: 0 }; // 否则滚到顶部(也可自定义逻辑)
    }
  }
});

关键点

  • savedPosition 只有在用户点击浏览器“前进/后退”按钮时才有值,编程式导航(this.$router.push)触发的路由切换,savedPositionnull,此时默认滚到顶部。
  • 该配置只处理浏览器导航栏的前进后退,若要覆盖“编程式导航+保留滚动”,得结合“手动保存滚动位置”逻辑。

(二)编程式导航场景:手动存/取滚动位置

如果是“从 A 页面 push 到 B 页面,再 back 到 A”这类编程式导航,需自己实现“保存-恢复”:

离开页面时保存滚动位置

路由守卫组件生命周期钩子中,记录当前滚动位置(如 window 的 scrollY,或可滚动 div 的 scrollTop)。

示例:用组件的 beforeRouteLeave 守卫保存到路由元信息(meta):

export default {
  beforeRouteLeave(to, from, next) {
    // 保存当前 window 滚动位置到来源路由的 meta
    from.meta.scrollTop = window.scrollY; 
    next();
  }
};

进入页面时恢复滚动位置

组件挂载后路由进入守卫中,读取保存的位置并恢复。

示例:用 onMounted 恢复(若用 Pinia/Vuex 存全局状态,逻辑类似):

export default {
  onMounted() {
    const scrollTop = this.$route.meta.scrollTop;
    if (scrollTop) {
      window.scrollTo(0, scrollTop); // 恢复滚动位置
    }
  }
};

注意:若组件实例未创建(比如用 beforeRouteEnter 守卫),需通过 next 传参访问组件实例:

beforeRouteEnter(to, from, next) {
  next(vm => { // vm 是组件实例
    if (from.meta.scrollTop) {
      window.scrollTo(0, from.meta.scrollTop);
    }
  });
}

(三)组件缓存场景:结合 keep-alive 的生命周期

当组件被 <keep-alive> 缓存时,路由切换不会销毁组件,而是触发 activated(组件激活)和 deactivated(组件停用时)钩子,保存-恢复”更高效——直接操作组件内的 DOM。

单组件滚动保持示例

假设有个需缓存的列表组件,内部有可滚动区域:

<template>
  <div class="scroll-container" ref="scrollContainer">
    <!-- 列表内容 -->
  </div>
</template>
<script setup>
import { ref, onDeactivated, onActivated } from 'vue';
const scrollContainer = ref(null);
let savedScrollTop = 0;
// 离开缓存时,保存滚动位置
onDeactivated(() => {
  savedScrollTop = scrollContainer.value.scrollTop;
});
// 重新激活时,恢复滚动位置
onActivated(() => {
  scrollContainer.value.scrollTop = savedScrollTop;
});
</script>
<style scoped>
.scroll-container {
  height: 500px;
  overflow-y: auto;
}
</style>

全局复用:封装 Mixin 或自定义指令

若多个组件需保持滚动,可封装 Mixin 复用逻辑:

// scrollKeepMixin.js
export const ScrollKeepMixin = {
  setup() {
    const scrollRef = ref(null);
    let savedTop = 0;
    onDeactivated(() => {
      if (scrollRef.value) {
        savedTop = scrollRef.value.scrollTop;
      }
    });
    onActivated(() => {
      if (scrollRef.value) {
        scrollRef.value.scrollTop = savedTop;
      }
    });
    return { scrollRef };
  }
};

组件中使用 Mixin:

<template>
  <div ref="scrollRef" class="scroll-wrap">...</div>
</template>
<script setup>
import { ScrollKeepMixin } from '@/mixins/scrollKeep';
const { scrollRef } = ScrollKeepMixin();
</script>

复杂场景:嵌套路由、多滚动容器

实际项目常遇到“嵌套路由”“页面内多个可滚动区域”等复杂情况,需针对性处理。

(一)嵌套路由的滚动保持

假设父路由组件有滚动区域,子路由切换时父组件不销毁(被缓存),此时父、子组件需分别处理滚动:

父组件(处理自身滚动容器):

<template>
  <div class="parent-scroll" ref="parentScroll">
    <router-view></router-view> <!-- 子路由出口 -->
  </div>
</template>
<script setup>
import { ref, onDeactivated, onActivated } from 'vue';
const parentScroll = ref(null);
let parentScrollTop = 0;
onDeactivated(() => {
  parentScrollTop = parentScroll.value.scrollTop;
});
onActivated(() => {
  parentScroll.value.scrollTop = parentScrollTop;
});
</script>

子组件:同理,用 deactivated/activated 处理自己的滚动容器。

(二)多滚动容器的区分保存

若页面内有多个可滚动区域(比如左右双列滚动),需给每个容器单独存/取 scrollTop

<template>
  <div ref="leftScroll" class="left">...</div>
  <div ref="rightScroll" class="right">...</div>
</template>
<script setup>
import { ref, onDeactivated, onActivated } from 'vue';
const leftScroll = ref(null);
const rightScroll = ref(null);
let leftTop = 0, rightTop = 0;
onDeactivated(() => {
  leftTop = leftScroll.value.scrollTop;
  rightTop = rightScroll.value.scrollTop;
});
onActivated(() => {
  leftScroll.value.scrollTop = leftTop;
  rightScroll.value.scrollTop = rightTop;
});
</script>

常见问题与避坑

实现滚动保持时,这些“小陷阱”要注意:

(一)scrollBehavior 不生效?

  • 确保路由模式是 createWebHistory()(hash 模式对 scrollBehavior 支持有限)。
  • savedPosition 仅在浏览器前进/后退时有效,编程式导航需手动存滚动位置。

(二)keep-alive 缓存导致恢复异常?

  • 需确保组件被 <keep-alive> 包裹(如 App.vue 中用 <keep-alive><router-view/></keep-alive>),否则 activated/deactivated 不会触发。

(三)异步组件加载时滚动恢复延迟?

异步组件(如 const Home = () => import('./Home.vue'))加载后,DOM 可能未完全渲染,恢复滚动时需用 nextTick 确保 DOM 就绪:

onActivated(() => {
  nextTick(() => {
    window.scrollTo(0, savedScrollTop);
  });
});

(四)移动端滚动兼容性问题?

iOS Safari 等移动端浏览器的滚动有“弹性效果”,恢复 scrollTop 时可能有偏差,可尝试用 scrollTobehavior: 'auto'(默认),或测试后调整滚动逻辑。

按场景选方案

不同场景对应不同技术方案,核心是“匹配需求+灵活组合工具”:

场景 推荐方案
浏览器前进/后退 scrollBehavior 配置,简单高效处理原生滚动位置。
编程式导航+组件不缓存 路由守卫(如 beforeRouteLeave)存滚动位置到 meta 或状态管理,进入时恢复。
组件被 keep-alive 缓存 deactivated/activated 钩子,在组件内或通过 Mixin 封装逻辑。
嵌套路由/多滚动容器 分别处理每个滚动区域,确保引用和保存逻辑一一对应。

实际项目中,往往是多种场景结合(比如列表页用 keep-alive 缓存,同时处理前进后退和编程式导航),这时候需把 scrollBehavior、路由守卫、组件生命周期钩子结合起来,才能覆盖所有情况。

掌握这些方法后,就能灵活处理“列表返回保留位置”“详情页返回保留浏览进度”等需求,让单页应用的体验更接近原生 App~

版权声明

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

发表评论:

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

热门