vue3 中配合 router 使用 keepalive
最近的一个需求中,要求可以将列表数据、筛选项、滚动位置记住,用户从列表进入详情又返回之后,或者前往其他页面又返回后,希望列表还在之前的位置,因为列表是滚动分页的,按目前的做法,从其他页面返回之后会重新刷新第一页的数据,要找到之前操作的数据比较麻烦。
这个诉求听到,第一时间想到就是 keepalive ,配合router 中的使用,将对应的列表页 alive 住,这样该页的数据自然都会留存,无需额外存储筛选项等数据。
模版配置
我的页面是一个嵌套路由,外层是一个 Layout 组件,Layout 被三个页面使用,模版配置如下。这里要注意
- vue3 中给 router-view 配置 keep-alive 要采用 slot 的方式
- include 中配置的是组件名字,如果没有在项目中额外配置,那么就是组件的文件名(我在项目中给每个组件都使用
defineOptions
进行了重命名)
<!-- App.vue 中的模版配置 -->
<router-view v-slot="{ Component }">
<keep-alive :include="['home-layout']" :max="10">
<component :is="Component"></component>
</keep-alive>
</router-view>
<!-- HomeLayout 中的模版配置 -->
<router-view v-slot="{ Component }">
<keep-alive :include="['clue-index']" :max="10">
<component :is="Component"></component>
</keep-alive>
</router-view>
记住滚动位置
keepalive 只是缓存了数据,并未记住滚动位置,所以切换页面之后还是在第一条,需要单独进行一个滚动位置的记住
会话期间滚动位置记住和设置的 hooks
主要作用是,在路由离开时计算当前的滚动距离,并存到 sessionStorage
中,暴露一个设置滚动高度的方法,用于从 sessionStorage
中获取滚动高度并设置,在列表页面的 onActivated
钩子中调用 setScrollTop
,将列表滚动到记住的位置。
export function useSaveScrollTop(key: string, className: string) {
onBeforeRouteLeave((from, to, next) => {
console.log('onBeforeRouteLeave')
saveScrollTop()
next()
})
function saveScrollTop() {
const listElement = document.querySelector(className); // 获取滚动的容器
const scrollTop = listElement?.scrollTop || 0; // 获取容器的scrollTop值
console.log(`保存滚动距离:${scrollTop}`)
sessionStorage.setItem(key, scrollTop.toString()); // 保存滚动位置
}
function setScrollTop() {
const savedPosition = sessionStorage.getItem(key);
if (savedPosition) {
const listElement = document.querySelector(className); // 获取滚动的容器
nextTick(() => {
if (listElement) {
console.log(`设置滚动距离:${savedPosition}`)
listElement.scrollTo(0, parseInt(savedPosition, 10)); // 恢复滚动位置
}
sessionStorage.removeItem(key); // 清除已恢复的滚动位置
});
}
}
return {
setScrollTop
}
}
新的问题 —— 切换卡顿
到上一步,记住位置的 case 其实算解决了,但是自测的时候,又发现了新的问题,当数据量到三五百条的时候,切换路由会有卡顿感,上千条数据时明显卡顿,这个应用主要运行在公司配备的工作手机上面,而我们工作手机的型号是 红米 note8 ,上千条数据时在我电脑上有两三秒卡顿,但是到工作手机上面,有十几秒的卡顿,笑死,这他么谁用。
和产品确认了下当前的数据量级,当前用户的列表数目大约在 500 左右,一个月之后估计能到千数量级,半年后估计到万数量级。考虑业务发展情况,万级先不必考虑,起码千级的数据还是要解决的。正常列表滚动加载,每页 50 条,即使加载上千条浏览也不会卡顿。但是使用了 keepalive 后,切换页面回来,可能需要一次性加载上千的 dom,这个压力还是挺大的。所以考虑使用虚拟列表解决这个问题。
尝试了几个插件,最后发现 https://github.com/Akryum/vue-virtual-scroller 的使用最方便,效果也最好。主要我当前的列表是使用了 vant 的 list 组件,并在 list 基础上封装了一个 list-common 组件做的加载,一般的组件没法达到很好的配合效果。
插件引入
为图方便,我直接全局引入了,毕竟就俩组件。
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
app.component('DynamicScroller', DynamicScroller)
app.component('DynamicScrollerItem', DynamicScrollerItem)
插件使用
<ListCommon
...参数略
>
<template v-slot="{ list }">
<DynamicScroller
class="clue-index_list-wrapper"
:items="list"
:min-item-size="200"
key-field="clueId"
page-mode
>
<template v-slot="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:size-dependencies="[
item.schoolStore,
item.distance
]"
:data-index="index"
>
<ClueListItem :source="item" @add-wechat="handleAddWechat" />
</DynamicScrollerItem>
</template>
</DynamicScroller>
</template>
</ListCommon>
添加之后,尝试上千条数据,切换照样丝滑,滚动位置也能很好的记住
新·新的问题 —— 单条数据更新需要
众嗦粥汁,详情页一般都伴随着编辑功能,那么新的问题出现了,进入到详情页编辑之后再返回,数据是没有更新的,从技术上来说很好理解,因为你这列表缓存了嘛,都缓存了咋给你更新;但是从用户角度来说:BUG!
解决思路:进入详情页的操作就是在 ListItem
中执行处理逻辑的,所以在 ListItem
中是知道当前操作的 id
的,当有进入详情的操作处理,记一下操作的 id
,在 ListItem
组件中的 onActivated
钩子中,判断若有记住的 id
,通过 id
获取一下详情,然后将详情数据变更到列表数据中,直接改了单项数据流中的引用数据(是的不推荐,但是这样最好用)。
至于为什么不判断一下有详情数据使用详情数据,没有详情数据使用列表 props
的数据。因为虚拟列表在滚动之后,之前的元素就销毁了,再次滚回去是重新进行了 mounted
挂载,所以如果是记录的详情数据是无法存储到的,只有单项数据流中的 list
数据是一直都在的。
代码略。
小结
所以列表缓存使用 keepalive + 虚拟列表的方案应该就是最优解了,用户体验也是非常好。好耶!