H5页面缓存策略---任务栈
技术栈: vue3+ts+pinia
应用场景
H5页面作为app的一部分时,常常通过webview进行展示.页面A切换到页面B时,会先卸载页面A,然后渲染页面B,这就导致两个问题:
- 页面A的数据都没有了,重新回到页面A需要重新加载.如果是表单,那么用户需要重新填写
- 页面切换不丝滑
这时如果将页面缓存起来,用户的体验会好很多.
问题
缓存页面的时候,如果缓存所有页面,对内存浪费是比较大的,但只缓存个别页面,如何保证用户体验是个问题,
解决方式
需要在内存损耗和用户体验之间所做一个权衡,可以模拟Android应用程序中的活动(Activity)生命周期:以栈的形式指定需要缓存的页面,页面切换时维护这个栈,具体的维护方式为:
- 进入首页,首页入栈: 当前栈[Home]
- 进入页面A,页面A入栈: 当前栈[Home,A]
- 又页面A进入页面B,页面B入栈: 当前栈[Home,A,B]
- 离开页面B,页面B出栈: 当前栈[Home,A]
- 离开页面A,进入首页,页面A出栈: 当前栈[Home]
这样当用户进行页面切换的时候,可以针对性的缓存响应页面,保证用户回退时,足够丝滑,同时减少内存损耗.
有了这个策略,那么实现思路就比较明确了:
- 使用
keepAlive组件实现页面缓存,以数组为include属性的值,来模拟栈,就可以只缓存栈中的页面 - 通过路由守卫,当路由切换时,对这个栈进行维护
实现
根据上面的思路,可以封装一个组件cacheRouterView,来替换掉原生提供的router-view.思考一下这个组件需要哪些属性和事件:
-
属性
- 由于需要判断页面的进入和离开,进入时向栈中
push,离开时pop因此需要一个属性来表示:routerChangeType: "push" | "back" | "none" - 当清空栈的时候,需要保持首页被缓存到,因此还需要知道首页的组件名字:
rootname:string
- 由于需要判断页面的进入和离开,进入时向栈中
-
事件: 不需要事件
最后使用组件代替router-view时应该是这样:
<cacheRouterView :routerChangeType='routerChangeType' rootName="Home" />
先把props定义以下:
const props = defineProps<{
routerChangeType: 'push' | 'back' | 'none';
rootName: string;
}>();
实现组件结构:
<router-view
v-slot="{ Component }"
>
<!-- 缓存组件 -->
<KeepAlive :include="cacheTask">
<component
:is="Component"
:key="$route.fullPath" // 兼容动态路由
/>
</KeepAlive>
</router-view>
实现缓存逻辑: 先定义一个响应式数据cacheTask表示任务栈,然后通过前置路由守卫维护这个栈
const router = useRouter();
const cacheTask = ref([props.rootName]);
router.beforeEach(to => {
const needCache = props.transitionName === 'push';
const needPop = props.transitionName === 'back';
if (needCache) {
cacheTask.value.push(to.name as string);
} else if (needPop) {
cacheTask.value.pop();
}
const isHomePage = to.name === props.rootName;
if (isHomePage) {
clearTask();
}
});
function clearTask() {
cacheTask.value = [props.rootName];
}
几行代码即可搞定,接下来使用一下:
注意点
-
路由名称需要和路由对应组件的名称相同: 因为向栈中存储时,存储的时
to.name,所有需要在组件中显示的指定组件名称<script> export default { name: 'Profile' } </script>
当前的路由结构:
const router: RouteRecordRaw[] = [
{
path: '/',
name: 'layoutMobile',
component: () => import('@/layout/mobile/index.vue'),
children: [
{
path: '/',
name: 'Home',// 要和import的组件名称对应
component: () => import('@/pages/home/index.vue')
},
{
path: '/profile',
name: 'Profile', // 要和import的组件名称对应
component: () => import('@/pages/profile/index.vue'),
meta: {
user: true
}
},
]
}
];
export default router;
定义一个全局状态routerChangeType,当进入或者离开页面时,改变这个状态,表示当次页面切换是push还是back,
export const useAppStore = defineStore('app', () => {
const state = ref<{ routerChangeType: 'none' | 'back' | 'push' }>({
routerChangeType: 'none'
});
const appState = computed(() => {
return state;
});
const setRouterChangeType = (val: 'none' | 'back' | 'push') => {
state.value.routerChangeType = val;
};
return {
appState,
setRouterChangeType
};
});
在路由入口中替换掉router-view,@/layout/mobile/index.vue中修改:
<template>
<div>
<MtransitionRouterView
:transitionName="appState.routerChangeType"
rootName="Home"
></MtransitionRouterView>
</div>
</template>
<script setup lang="ts">
import { useAppStore } from '@/stores/modules/app';
const { appState } = useAppStore();
</script>
当应用中页面切换时,修改全局状态:
import { useAppStore } from '@/stores/modules/app';
const appStore = useAppStore();
const { setRouterChangeType } = appStore;
...
setRouterChangeType('back');
router.back();
或者
setRouterChangeType('push');
router.push('/Profile');
...
至此,缓存策略的实现和使用已经全部ok了,切换页面,再回退,既可以看到回退的页面并没有重新加载