项目背景
项目使用qiankun进行了页面集成,要求实现父子应用切换tab时实现页面缓存。
项目框架
vue + qiankun + vuex + vue-router + elementUi
主子应用都使用vue,路由统一使用history模式
实现原理
路由变化时,判断是否是子应用路由并且是否没有加载过子应用,是的话则手动加载子应用。删除tab时是子应用并且在tablist中是否还有子应用菜单,如果没有,则unmount方法卸载子应用,如果有子应用菜单,则执行update方法清除该子应用的组件缓存。
实现方案
主应用
通过监听主应路由的beforeEach方法来动态加载(loadMicroApp)微应用页面,使用vuex管理已加载的tab标签和微应用数据。
index.ts
/**每次路由切换之前,先注册微应用 */
createMicroApp(to).then(() => {
next();
});
子应用
组件开启keep-alive,由于keep-alive只能支持二级及以下路由,对于三级及以上路由,keep-alive失效,所以需要对keep-alive重新封装,并且在qiankun提供的生命周期钩子update里调用子应用删除缓存的方法 用于清除子应用组件keep-alive缓存。
BaseKeepAlive.js
/**
* base-keep-alive 主要解决问题场景:多级路由缓存
* 前提:保证动态路由生成的route name 值都声明了且唯一
* 基于以上对keep-alive进行以下改造:
* 1\. 组件名称获取更改为路由名称
* 2\. cache缓存key也更改为路由名称
* 3\. pruneCache
*/
const _toString = Object.prototype.toString;
// 类型判断
function isRegExp(v) {
return _toString.call(v) === '[object RegExp]';
}
// 删除指定项
export function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
/**
* 1\. 主要更改了 name 值获取的规则 改为获取路由name
* @param {*} opts
*/
function getComponentName(opts) {
return this.$route.name;
}
function isDef(v) {
return v !== undefined && v !== null;
}
function isAsyncPlaceholder(node) {
return node.isComment && node.asyncFactory;
}
// 获取有效的子级组件
function getFirstComponentChild(children) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i];
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c;
}
}
}
}
// 匹配缓存配置是否符合规范
function matches(pattern, name) {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1;
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1;
} else if (isRegExp(pattern)) {
return pattern.test(name);
}
/* istanbul ignore next */
return false;
}
// 处理缓存配置
function pruneCache(keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance;
for (const key in cache) {
const cachedNode = cache[key];
if (cachedNode) {
// ------------ 3\. 之前默认从router-view取储存key值, 现在改为路由name, 所以这里得改成当前key
const name = key;
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode);
}
}
}
}
// 处理缓存配置项
function pruneCacheEntry(cache, key, keys, current) {
const cached = cache[key];
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy();
}
cache[key] = null;
remove(keys, key);
}
const patternTypes = [String, RegExp, Array];
export default {
name: 'keep-alive',
// abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created() {
this.cache = Object.create(null);
this.keys = [];
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted() {
// 监听缓存页面name
this.$watch('include', (val) => {
pruneCache(this, (name) => matches(val, name));
});
// 监听非缓存页面name
this.$watch('exclude', (val) => {
pruneCache(this, (name) => !matches(val, name));
});
},
render() {
const slot = this.$slots.default;
const vnode = getFirstComponentChild(slot);
const componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
const name = getComponentName.call(this, componentOptions);
// ---------- 对于没有name值得设置为路由得name, 支持vue-devtool组件名称显示
if (!componentOptions.Ctor.options.name) {
vnode.componentOptions.Ctor.options.name;
}
const { include, exclude } = this;
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode;
}
const { cache, keys } = this;
// ------------------- 储存的key值, 默认从router-view设置的key中获取
const routerkey =
vnode.key == null
? componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key;
// 判断path是否存在参数,存在参数则刷新
const haveParams = routerkey.includes('?');
// ------------------- 2\. 储存的key值设置为路由中得name值
const key = name;
if (cache[key] && !haveParams) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key);
keys.push(key);
} else {
cache[key] = vnode;
keys.push(key);
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0]);
}
};
keepalive.modules.js
const keepalive = {
state: {
keepalive: []
},
mutations: {
setKeepalive(state, val) {
state.keepalive = val;
},
clearKeepalive(state, val) {
state.keepalive = state.keepalive.filter((item) => !val.includes(item));
}
},
actions: {
setKeepalive(context, value) {
context.commit('setKeepalive', value);
},
clearKeepalive(context, value) {
context.commit('clearKeepalive', value);
}
}
};
export default keepalive;
getters
getKeepalive: (state: any) => state.keepalive.keepalive,
main.js
// KeepAlive
import KeepAlive from './BaseKeepAlive';
Vue.component('BaseKeepAlive', KeepAlive);
export async function update(props) {
console.log('子应用更新:');
const { props: qiankunProps } = props;
if (qiankunProps && qiankunProps.type === 'closeTab') {
store?.commit('clearKeepalive', qiankunProps.pathList);
}
}
路由全局前置守卫
const keepaliveStore = store.getters.getKeepalive;
let keepAlive = keepaliveStore || [];
if (
to.name &&
to.meta &&
to.meta.keepAlive &&
!keepAlive.includes(to.name)
) {
keepAlive.push(to.name);
store?.commit('setKeepalive', keepAlive);
}
content.vue
<div>
<transition name="fade-transform" mode="out-in">
<base-keep-alive :include="keepalive">
<router-view
v-if="keepFlag"
:key="$route.fullPath"
class="contentInnerChild"
></router-view>
</base-keep-alive>
</transition>
<transition name="fade-transform" mode="out-in">
<router-view
v-if="!keepFlag"
:key="$route.fullPath"
class="contentInnerChild"
/>
</transition>
</div>
computed: {
...mapState({
keepalive: (state) => {
return state.keepalive.keepalive;
}
}),
keepFlag() {
return this.$route.meta?.keepAlive;
}
},
到此为止子项目已经能实现页面级缓存了,嵌入到子项目中的时候发现切换这个子项目的其他页签都能缓存但是切换到主项目或者其他子项目页面缓存失效,这时候需要把上面content.vue中的代码同步放到App.vue中。