在Vue单页应用开发中,我们经常会遇到这样的场景:用户在一个长列表页面滚动浏览了若干项后,点击进入详情页,然后返回期望能够回到之前的滚动位置,而不是重新回到页面顶部。这种用户体验的优化对于内容型应用尤为重要。
Vue的<keep-alive>
组件可以缓存页面状态,但它并不自动保存和恢复滚动位置。今天我们就来探讨一种优雅的解决方案。
核心代码解析
javascript
import { onActivated, ref } from "vue";
import { onBeforeRouteLeave } from "vue-router";
export function useScroll(targetRef) {
const scrollTop = ref(0);
onActivated(() => {
if (targetRef.value) {
targetRef.value.scrollTop = scrollTop.value
}
})
onBeforeRouteLeave(() => {
if (targetRef.value) {
scrollTop.value = targetRef.value.scrollTop
}
})
}
这个自定义组合式函数useScroll
虽然代码简洁,但功能强大且完整。让我们分解一下它的工作原理:
1. 响应式状态管理
使用ref(0)
创建了一个响应式的scrollTop
变量,用于存储滚动位置。
2. 生命周期钩子运用
onActivated
: 当被<keep-alive>
缓存的组件激活时调用,用于恢复滚动位置onBeforeRouteLeave
: 在路由离开之前调用,用于保存当前滚动位置
3. 引用DOM元素
通过targetRef
参数接收一个DOM元素的引用,这使得函数可以灵活应用于任何可滚动元素
完整使用示例
vue
<template>
<div class="product-list" ref="scrollTarget">
<div v-for="product in products" :key="product.id" class="product-item">
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
<button @click="goToDetail(product)">查看详情</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useScroll } from '@/composables/useScroll'
const router = useRouter()
const scrollTarget = ref(null)
const products = ref([...]) // 产品列表数据
// 使用滚动记忆功能
useScroll(scrollTarget)
const goToDetail = (product) => {
router.push(`/product/${product.id}`)
}
</script>
<style scoped>
.product-list {
height: 100vh;
overflow-y: auto;
}
</style>
路由配置要点
为了使<keep-alive>
生效,需要在路由配置和组件渲染中做相应设置:
javascript
// router.js
const routes = [
{
path: '/products',
component: () => import('@/views/ProductsView.vue'),
meta: { keepAlive: true } // 添加元信息标识需要缓存的页面
}
// ...其他路由
]
vue
<!-- App.vue -->
<template>
<router-view v-slot="{ Component, route }">
<keep-alive>
<component
:is="Component"
v-if="route.meta.keepAlive"
:key="route.name"
/>
</keep-alive>
<component
:is="Component"
v-if="!route.meta.keepAlive"
:key="route.name"
/>
</router-view>
</template>
进阶优化
1. 多滚动容器支持
实际应用中,一个页面可能有多个滚动区域,我们可以扩展useScroll
来支持这种情况:
javascript
export function useScroll(targetRef, identifier = 'default') {
const scrollPositions = ref({});
onActivated(() => {
if (targetRef.value) {
targetRef.value.scrollTop = scrollPositions.value[identifier] || 0;
}
})
onBeforeRouteLeave(() => {
if (targetRef.value) {
scrollPositions.value[identifier] = targetRef.value.scrollTop;
}
})
}
2. 防抖处理
对于高频触发滚动事件的情况,可以添加防抖优化:
javascript
import { debounce } from 'lodash-es';
export function useScroll(targetRef) {
const scrollTop = ref(0);
onActivated(() => {
if (targetRef.value) {
targetRef.value.scrollTop = scrollTop.value;
}
})
const saveScrollPosition = debounce(() => {
if (targetRef.value) {
scrollTop.value = targetRef.value.scrollTop;
}
}, 100);
onMounted(() => {
if (targetRef.value) {
targetRef.value.addEventListener('scroll', saveScrollPosition);
}
});
onUnmounted(() => {
if (targetRef.value) {
targetRef.value.removeEventListener('scroll', saveScrollPosition);
}
});
}