H5页面缓存策略---任务栈

543 阅读3分钟

H5页面缓存策略---任务栈

技术栈: vue3+ts+pinia

应用场景

H5页面作为app的一部分时,常常通过webview进行展示.页面A切换到页面B时,会先卸载页面A,然后渲染页面B,这就导致两个问题:

  1. 页面A的数据都没有了,重新回到页面A需要重新加载.如果是表单,那么用户需要重新填写
  2. 页面切换不丝滑

这时如果将页面缓存起来,用户的体验会好很多.

问题

缓存页面的时候,如果缓存所有页面,对内存浪费是比较大的,但只缓存个别页面,如何保证用户体验是个问题,

解决方式

需要在内存损耗和用户体验之间所做一个权衡,可以模拟Android应用程序中的活动(Activity)生命周期:以栈的形式指定需要缓存的页面,页面切换时维护这个栈,具体的维护方式为:

  1. 进入首页,首页入栈: 当前栈[Home]
  2. 进入页面A,页面A入栈: 当前栈[Home,A]
  3. 又页面A进入页面B,页面B入栈: 当前栈[Home,A,B]
  4. 离开页面B,页面B出栈: 当前栈[Home,A]
  5. 离开页面A,进入首页,页面A出栈: 当前栈[Home]

image-20240702235026903.png

这样当用户进行页面切换的时候,可以针对性的缓存响应页面,保证用户回退时,足够丝滑,同时减少内存损耗.

有了这个策略,那么实现思路就比较明确了:

  1. 使用keepAlive组件实现页面缓存,以数组为include属性的值,来模拟栈,就可以只缓存栈中的页面
  2. 通过路由守卫,当路由切换时,对这个栈进行维护

实现

根据上面的思路,可以封装一个组件cacheRouterView,来替换掉原生提供的router-view.思考一下这个组件需要哪些属性和事件:

  1. 属性

    1. 由于需要判断页面的进入和离开,进入时向栈中push,离开时pop因此需要一个属性来表示: routerChangeType: "push" | "back" | "none"
    2. 当清空栈的时候,需要保持首页被缓存到,因此还需要知道首页的组件名字: rootname:string
  2. 事件: 不需要事件

最后使用组件代替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];
}

几行代码即可搞定,接下来使用一下:

注意点

  1. 路由名称需要和路由对应组件的名称相同: 因为向栈中存储时,存储的时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了,切换页面,再回退,既可以看到回退的页面并没有重新加载