使用场景
在做项目时遇到需要缓存页面,比如 A页面 -> B列表页面 —> C详情页面
这时 C详情页面返回到B列表页面时需要缓存,B列表页面到A页面时不需要缓存。
踩过的坑
问题: 动态设置 路由的 meta 属性 keepAlive 时,发现总是第一次不生效,需要进行第二次跳转才能生效
原因: 大概率是你采用了以下的设置方法
// app.vue
<template>
<router-view v-slot="{ Component }">
<KeepAlive>
<component :is="Component" :key="$route.fullPath" v-if="$route.meta.keepAlive" />
</KeepAlive>
<component :is="Component" :key="$route.fullPath" v-if="!$route.meta.keepAlive" />
</router-view>
</template>
因为使用v-if $route.meta.keepAlive 来判断是否缓存,当第一次进入页面时其实是并没有设置成功的。
最好看下 vue 的文档,有章节专门介绍 include 和 exclude 。 利用这俩个参数就可以完美实现动态设置缓存页面
实践方案:
- 设置 KeepAlive 路由
// app.vue
<template>
<router-view v-slot="{ Component }">
<KeepAlive :include="includePages">
<component :is="Component" :key="$route.fullPath" />
</KeepAlive>
</router-view>
</template>
<script lang="ts" setup>
/**
* @description: 设置缓存页面逻辑
*/
const keepPageStore = useKeepPageStoreWithOut()
const { includePages } = storeToRefs(keepPageStore)
</script>
- 使用 状态管理工具 来进行 动态设置 include
// store
import { defineStore } from 'pinia'
import { store } from '../index'
export const useKeepPageStore = defineStore({
id: 'KeepPage',
state: () => ({
includePages: [] as string[],
scrollTop: 0
}),
actions: {
addIncludePage(pageName: string) {
if (!this.includePages.includes(pageName)) {
this.includePages.push(pageName)
}
// 记住当前的滚动距离
this.scrollTop = window.scrollY
},
removeIncludePage(pageName: string) {
this.includePages = this.includePages.filter((item) => item !== pageName)
this.scrollTop = 0
}
}
})
export function useKeepPageStoreWithOut() {
return useKeepPageStore(store)
}
- 写一个通用的 hooks 用来更加方便的复用代码逻辑
// hooks
import { onBeforeRouteLeave } from 'vue-router'
import { useKeepPageStoreWithOut } from '~/store/modules/keepPage'
const keepPageStore = useKeepPageStoreWithOut()
// 这里把targetPages参数设置为数组,因为目标页面可能有多个,比如B -> C, B -> D
export function useKeepPage(targetPages: string[] | 'all') {
/**
* @description: 设置页面缓存
* @param {toName} string 目标页面name
* @param {formName} string 来源页面name
* @return {*}
*/
const setKeepPage = (toName: string, formName: string) => {
// 判断是否进入目标页面
if (targetPages === 'all') {
// 如果为 all 代表 则目标页面跳到到 任何页面再返回过来时都需要被缓存
return keepPageStore.addIncludePage(formName)
}
if (!targetPages.includes(toName)) {
// 当前页面不需要缓存目标页面,则就移除在 include 中移除目标页面
keepPageStore.removeIncludePage(formName)
} else {
// 当前面需要缓存目标页面,则就在 include 中添加目标页面
keepPageStore.addIncludePage(formName)
}
}
// 在目标页面离开时触发
onBeforeRouteLeave((to, form) => {
setKeepPage(to.name as string, form.name as string)
})
}
- 最后在需要缓存的页面里 用 useKeepPage 这个hook函数即可
// 需要缓存的页面使用
keepPageStore(['C详情页面的路由名']) // 比如用在B列表页面,跳到C详情页面再返回过来 此时B列表页面就是缓存的了, 如果从其他页面返回过来的,B列表页面就不是缓存的
- 返回目标页面时 滚动到上一次离开时的位置
// router.ts
import { createRouter, createWebHistory, RouterScrollBehavior } from 'vue-router'
import routes from 'virtual:generated-pages'
const keepPageStore = useKeepPageStoreWithOut()
const scrollBehavior: RouterScrollBehavior = () => {
// 异步滚动操作
return new Promise((resolve) => {
setTimeout(() => {
resolve({ left: 0, top: keepPageStore.scrollTop })
}, 0)
})
}
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior
})
待优化地方
看上述方案,其实都可以把所有逻辑都写在 一个hook 函数里,不用多此一举再写一个状态管理去保存 include。 (说明下,我这样写是为了在测试时 可以更加方便的用 Vue Devtools 工具查看)
结尾
如果有比较细心的小伙伴会发现,在第五步时,我并没有用到 scrollBehavior 自带的第三个参数 savePosition, 而是用的自己实现记住位置的变量(scrollTop)。
其实在测试过程中发现,用该函数自带的 savePosition参数 在一些复杂场景下并不准确。
比如: B列表页面 -> C详情页面(假设此时的B列表页面的滚动距离为 1000)-> B列表页面(滚动距离:1000) -> A页面 -> B列表页面 -> C详情页面(假设此时的B列表页面的滚动距离为 2000) -> B列表页面 (此时的滚动距离应该为 2000, 但实际 savePosition 存的top值为 1000)