[qiankun]-多页签缓存
- 环境
- 功能需求
- 多页签缓存方案
- 方案1.主服务进行html替换
- 方案2.微服务vnode 替换
- 方案3.每个微服务都不卸载
- 微服务加载方式的选择
- 微服务的路由路径选择
- 微服务的缓存工具
- 微服务的容器
- 使用tab作为微服务的挂载容器
- 使用微服务路由作为微服务的挂载容器
- 场景描述
- 微服务的缓存
- 缓存微服务
- 删除微服务
- 不同路由同一微服务情况
管理系统比较常用的一个功能就时多页签的缓存,我们通过缓存已经打开的页签,并在切换页签的时候看到之前的查询结果,在关闭页签的重新点击菜单那时,看到的是新的没有查询记录界面
环境
语言:采用的主流的vue3.x+ts开发
UI框架:使用的是ant-design框架
其它工具:使用了微服务技术-qiankun
缓存工具:keep-alive
功能需求
-
通过点击菜单,打开一个菜单页签(点击后展示的菜单称之为页签):
如果点击菜单之前已打开页签中没有该菜单,则该页面应该是初始页面状态;
如果点击菜单之前已打开页签中有该菜单,则该页面应该是上次操作后状态;
-
切换已打开的页签:
则页面应该都是上次操作后状态;
-
关闭页签后再次点击菜单:
因为点击菜单之前关闭页签,所以已打开页签中没有该菜单,则该页面应该是初始页面状态;
多页签缓存方案
方案1.主服务进行html替换
通过存储每个微服务容器的html内容,然后在切回当前页签的时候,微服务容器使用缓存的html替换,也就是所谓的替换innerHTML。
方案尝试结果描述:
替换的内容,使得切换回原有页面确实是上次的查询结果页面,但是页面失去了响应性,变成了静态的HTML页面,包括下拉框都没有了
方案2.微服务vnode 替换
通过存储每个微服务容器的 vnode 内容,然后在切回当前页签的时候,创建微服务实例的时候,使用vnode替换掉原有的render渲染函数
方案尝试结果描述:
因为本人使用的vue3开发,使用render渲染函数是vue2创建vue实例的方法,因此该方案无法实现使用vnode替换掉原有的render渲染函数
尝试了vnode的缓存强制替换的可能性,vue3提示_vnode是只读属性不可以设置
方案3.每个微服务都不卸载
以上两个方案,都是基于同时只加载一个微服务,并且在切换的时候卸载当前微服务,之所以先尝试前两个方案也是因为,如果页面每个微服务都不卸载,当打开的页签过多时会有性能问题,因为微服务总数量暂时有限,该问题此时先不考虑了,如果之后有需要会另外总结
因为前两个方案都不可行,只能尝试该方案,方案描述:
微服务加载方式的选择
qiankun加载加载微服务的方式有两种:
- 是注册加载,通过劫持路由,会根据路由变化加载每个微服务
registerMicroApps,是根据路由的变化加载微服务的,只要路由变化,就会触发微服务重新加载微服务,因此无法阻止微服务的重新加载,缓存不能被使用,指定容器的内容会被覆盖,因此感觉不适合缓存页签的方案 - 是手动加载,可以自己决定什么时候加载
loadMicroApp ,是手动加载,因此能决定是重新加载,还是使用缓存,感觉适合使用该方式
因此选择手动加载微服务的方式
微服务的路由路径选择
主服务具有公共页面,例如404,intro等路由页面,公共页面与微服务都是展示在页面的主体区,并且不需要缓存,因为没有操作功能,当然缓存也是可以的。微服务展示的时候,公共路由页面不可展示,路由也需要变为微服务的路由,不能使用之前的路由路径。
因此可以使用router.push(),或者router.replace()改变浏览器的路由路径,有说可以把路由router想象成一个访问记录的栈,router.replace() 是替换掉栈顶,而router.push() 则是向栈中再堆入一个新记录
考虑过所有微服务使用同一个路由路径,但是因为浏览器的前进后退记录管理服务,因此每个微服务的路由路径不能完全相同,否则无法区分微服务的前进后退记录,所以也不能使用replace方法了
因此每个微服务页面具有一个唯一的路由路径,需要使用router.push记录所有的路由路径,并且需要用来加载微服务的路由组件(如果没有找到指定的路由,系统会跳转404,因此需要微服务的路由组件,微服务众多,因此所有微服务可以采用相同的前缀,用以匹配到同一个路由组件),一个微服务路由组件就能够满足需求
相同的前缀,/micro/XXXX/XXXX,例如:/micro/service/function
{
path: "/micro/:pathMatch(.*)*",///micro/:projectName/:page
meta: { auth: true },
component: () => import(/* webpackChunkName: "micro" */ "../micro/index.vue")
}
微服务的缓存工具
默认情况下,一个组件实例在被替换掉后会被销毁。这会导致它丢失其中所有已变化的状态 —— 当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。
keep-alive 是 vue 内置的动态组件缓存工具
keep-alive 缓存路由的key默认是每个路由的name,一般的路由缓存因为每个路由设置了不同的name,所以include包含需要缓存的路由的name即可
那么针对想更改缓存Key的情况:
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachePannels">
<component :is="wrap(route, Component)" :key="wrapKey(route.path)" />
</keep-alive>
</router-view>
以下对component与key的处理,PROJECT.cacheKey设置缓存的key字段:
const wrap = (route, component) => {
component.type.name = "path" == PROJECT.cacheKey ? route.path : store.state.pathMap[route.path].id;
return component
};
const wrapKey = (path) => {
if ("path" == PROJECT.cacheKey) {
return path
} else {
return store.state.pathMap[path].id
}
}
经过测试发现若缓存的字段是path,则缓存成功,若是key是改成id则缓存不成功,因为对于源码的理解并不深入,所以目前并不理解原因,也暂时不做深入,这里还是想吐槽一句,看到在源码处这个问题已经被提出几年了,但是都没有一个官方正式的回应,都是小伙伴提供的各种解决方案,而且就像上面说的绑定的key还必须是path其它不可以。。。
好吧,暂时不纠结了,既然path可用,就确定使用path,以下是最终代码
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachePannels">
<component :is="wrap(route, Component)" :key="route.path" />
</keep-alive>
</router-view>
const wrap = (route, component) => {
component.type.name = route.path;
return component
};
因为微服务使用的是同一个路由组件,因此name相同,那么正好需要使用根据key进行缓存的功能
基于上述原因,使用keep-alive缓存,路由path,作为缓存的key。
关于keep-alive重写的功能以后有机会在尝试吧
微服务的容器
一开始我是倾向于直接用微服务路由组件作为加载微服务的容器
但是使用路由的path作为路由缓存的key,理论讲一旦path发生变更,则路由组件会被销毁重新加载,但是微服务切换时到底组件是否会被销毁?在尝试时存在一些无可避免的问题,当时没有成功
使用tab作为微服务的挂载容器
这里先讲述一个成功缓存的方式
<template>
<a-tabs v-model:activeKey="activeKey"
@tabClick="clickTab" @edit="closeTab">
<a-tab-pane v-for="item in cacheList" :key="item.id" :tab="item.title">
<div :id="'micro'+item.id" class="micro-tab"></div>
</a-tab-pane>
</a-tab>
<router-view v-slot="{ Component, route }">
<keep-alive :include="cachePannels">
<component :is="wrap(route, Component)" :key="route.path" />
</keep-alive>
</router-view>
</template>
微服务路由 micro.vue
<template>
</template>
<script lang="ts" setup>
import { useStore } from "store";
const store = useStore();
store.commit("loadMicroApp");
</script>
注意,虽然微服务加载是在tab中挂载的,但是router-view此时是要正常加载微服务路由的,因为路径要有对应的匹配路由,但是微服务又不在其中挂载,所以隐藏微服务路由 micro.vue 的展示即可
使用微服务路由作为微服务的挂载容器
加载的时候,因为使用的path作为key,理论来讲,keep-alive应该将每个微服务作为一个单独的路由进行缓存.
但是实际情况有所不同,现象描述:
在打开页签的过程中,每次都是onMount了的,切换的时候,可以看到确实缓存了微服务,每次切换的时候显示的是缓存页面
但是在关闭页签的时候出现了问题,一旦关闭了其中一个正在展示的微服务,其它微服务页面就白屏了,就像是其它路由组件都被删除了?!所以为什么作为单独路由缓存的其它页面没有正常展示?但是如果关闭的是没有展示的页签,则并不影响当前路由的展示与后续的切换
因为该问题没有解决,所以最终没有采用该方式!!下面采用路由组件作为容器时的现象
场景描述
主服务首次点击菜单A,打开页签A,挂载A服务 onMounted onActivated
主服务首次点击菜单B,打开页签B,挂载B服务 onMounted onActivated
切换A服务,B onDeactivated,A onActivated
切换B服务,A onDeactivated,B onActivated
此时关闭B服务:
实际:B onUnmounted,A onMounted onActivated
理论:B onUnmounted,A onActivated
这就像是A挂载到了B的路由组件上了?
正常的切换时keep-alive缓存的组件
vue 的组件,不同路径,同一个路由, 缓存的组件为同一个
因此同组件路由卸载的时候,另外的就无法展示了,原因找到了,但是如何区分开?尝试过使用cloneVnode 克隆micro.vue组件,但是无效,所以暂时未找到解决方案
微服务的缓存
缓存微服务
需要缓存的微服务正常手动加载微服务即可,并缓存该微服务
最早的时候是直接缓存的
cacheMap[microApp.path] = loadMicroApp(microApp)
后来在实践中发现直接缓存是存在问题的,例如当loadMicroApp失败的时候(微服务不存在,不可用的情况),此时缓存的微服务实例是不存在的,自然对于该微服务实例后续的卸载会导致报错
因此,当缓存微服务的时候需要确保微服务加载成功:
const appMicro = loadMicroApp(microApp, { singular: false }, microLifeCycle);
appMicro.mountPromise.then((res) => {
//微服务加载成功时保存微服务
state.micro.cacheMap[microApp.path] = appMicro;
}).catch((error) => {
//微服务加载失败时提示错误
console.error(`mountPromise error`, error)
})
microLifeCycle是微服务的生命周期,可以在其中设置微服务的加载动画
删除微服务
关闭页签,也就是删除缓存微服务时,注意一定要先卸载微服务,unmount,否则下次加载该微服务时会报已挂载错误
//卸载微服务
cacheMap[microApp.path].unmount();
//删除微服务实例
delete cacheMap[microApp.path]
不同路由同一微服务情况
如果微服务有两个路由,而两个路由页面都点击后要缓存,此时也是正常加载即可,当作两个不同微服务加载即可,也不会报已挂载错误,因为路径不同可以当作缓存的是两个微服务(不同于在micro.vue中挂载,该路由组件中如果当作两个微服务挂载是会报已挂载错误的),因为是加载在tab中,彼此并不互相影响