Browse Source

feat: add the attribute routeCached to route to control cache the DOM corresponding to the route

pull/7586/head
Jin Mao 4 weeks ago
parent
commit
bd22793ceb
  1. 7
      docs/src/guide/essentials/route.md
  2. 4
      packages/@core/base/typings/src/vue-router.d.ts
  3. 102
      packages/effects/layouts/src/basic/content/content.vue
  4. 98
      packages/effects/layouts/src/hooks/index.ts
  5. 2
      packages/effects/layouts/src/route-cached/index.ts
  6. 36
      packages/effects/layouts/src/route-cached/route-cached-page.vue
  7. 98
      packages/effects/layouts/src/route-cached/route-cached-view.vue
  8. 36
      packages/stores/src/modules/tabbar.ts
  9. 1
      playground/src/router/routes/modules/demos.ts

7
docs/src/guide/essentials/route.md

@ -599,6 +599,13 @@ _注意:_ 排序仅针对一级菜单有效,二级菜单的排序需要在对
用于配置当前路由不使用基础布局,仅在顶级时生效。默认情况下,所有的路由都会被包裹在基础布局中(包含顶部以及侧边等导航部件),如果你的页面不需要这些部件,可以设置 `noBasicLayout``true` 用于配置当前路由不使用基础布局,仅在顶级时生效。默认情况下,所有的路由都会被包裹在基础布局中(包含顶部以及侧边等导航部件),如果你的页面不需要这些部件,可以设置 `noBasicLayout``true`
### domCached
- 类型:`boolean`
- 默认值:`false`
用于配置当前路由是否要将route对应dom元素缓存起来。对于一些复杂页面切换tab浏览器回流/重绘会导致卡顿, `domCached` 设为 `true`可解决该问题,但是也有代价:1、内存占用升高 2、vue的部分生命周期不会触发
## 路由刷新 ## 路由刷新
路由刷新方式如下: 路由刷新方式如下:

4
packages/@core/base/typings/src/vue-router.d.ts

@ -43,6 +43,10 @@ interface RouteMeta {
| 'success' | 'success'
| 'warning' | 'warning'
| string; | string;
/**
* dom是否缓存起来
*/
domCached?: boolean;
/** /**
* keytrue * keytrue
*/ */

102
packages/effects/layouts/src/basic/content/content.vue

@ -1,17 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VNode } from 'vue'; import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
import type {
RouteLocationNormalizedLoaded,
RouteLocationNormalizedLoadedGeneric,
} from 'vue-router';
import { computed } from 'vue'; import { unref } from 'vue';
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import { preferences, usePreferences } from '@vben/preferences'; import { usePreferences } from '@vben/preferences';
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores'; import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
import { transformComponent, useLayoutHook } from '../../hooks';
import { IFrameRouterView } from '../../iframe'; import { IFrameRouterView } from '../../iframe';
import { RouteCachedPage, RouteCachedView } from '../../route-cached';
defineOptions({ name: 'LayoutContent' }); defineOptions({ name: 'LayoutContent' });
@ -21,85 +19,27 @@ const { keepAlive } = usePreferences();
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } = const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
storeToRefs(tabbarStore); storeToRefs(tabbarStore);
/** const { getEnabledTransition, getTransitionName } = useLayoutHook();
* 是否使用动画
*/
const getEnabledTransition = computed(() => {
const { transition } = preferences;
const transitionName = transition.name;
return transitionName && transition.enable;
});
//
function getTransitionName(_route: RouteLocationNormalizedLoaded) {
// 使
const { tabbar, transition } = preferences;
const transitionName = transition.name;
if (!transitionName || !transition.enable) {
return;
}
// 使
if (!tabbar.enable || !keepAlive) {
return transitionName;
}
// 使
// if (route.meta.loaded) {
// return;
// }
// 使
// const inTabs = getCachedTabs.value.includes(route.name as string);
// return inTabs && route.meta.loaded ? undefined : transitionName;
return transitionName;
}
/** /**
* 转换组件自动添加 name * 是否显示component
* @param component * @param route
*/ */
function transformComponent( const showComponent = (route: RouteLocationNormalizedLoadedGeneric) => {
component: VNode, return !route.meta.domCached && unref(renderRouteView);
route: RouteLocationNormalizedLoadedGeneric, };
) {
//
if (!component) {
console.error(
'Component view not found,please check the route configuration',
);
return undefined;
}
const routeName = route.name as string;
// name
if (!routeName) {
return component;
}
const componentName = (component?.type as any)?.name;
// name
if (componentName) {
return component;
}
// componentName routeName
if (componentName === routeName) {
return component;
}
// name
component.type ||= {};
(component.type as any).name = routeName;
return component;
}
</script> </script>
<template> <template>
<div class="relative h-full"> <div class="relative h-full">
<IFrameRouterView /> <IFrameRouterView />
<RouteCachedView />
<RouterView v-slot="{ Component, route }"> <RouterView v-slot="{ Component, route }">
<RouteCachedPage
:component="Component"
:route="route"
v-if="route.meta.domCached"
/>
<Transition <Transition
v-if="getEnabledTransition" v-if="getEnabledTransition"
:name="getTransitionName(route)" :name="getTransitionName(route)"
@ -113,14 +53,14 @@ function transformComponent(
> >
<component <component
:is="transformComponent(Component, route)" :is="transformComponent(Component, route)"
v-if="renderRouteView" v-if="showComponent(route)"
v-show="!route.meta.iframeSrc" v-show="!route.meta.iframeSrc"
:key="getTabKey(route)" :key="getTabKey(route)"
/> />
</KeepAlive> </KeepAlive>
<component <component
:is="Component" :is="Component"
v-else-if="renderRouteView" v-else-if="showComponent(route)"
:key="getTabKey(route)" :key="getTabKey(route)"
/> />
</Transition> </Transition>
@ -132,14 +72,14 @@ function transformComponent(
> >
<component <component
:is="transformComponent(Component, route)" :is="transformComponent(Component, route)"
v-if="renderRouteView" v-if="showComponent(route)"
v-show="!route.meta.iframeSrc" v-show="!route.meta.iframeSrc"
:key="getTabKey(route)" :key="getTabKey(route)"
/> />
</KeepAlive> </KeepAlive>
<component <component
:is="Component" :is="Component"
v-else-if="renderRouteView" v-else-if="showComponent(route)"
:key="getTabKey(route)" :key="getTabKey(route)"
/> />
</template> </template>

98
packages/effects/layouts/src/hooks/index.ts

@ -0,0 +1,98 @@
import type { VNode } from 'vue';
import type {
RouteLocationNormalizedLoaded,
RouteLocationNormalizedLoadedGeneric,
} from 'vue-router';
import { computed } from 'vue';
import { preferences, usePreferences } from '@vben/preferences';
/**
* name
* @param component
* @param route
*/
export function transformComponent(
component: VNode,
route: RouteLocationNormalizedLoadedGeneric,
) {
// 组件视图未找到,如果有设置后备视图,则返回后备视图,如果没有,则抛出错误
if (!component) {
console.error(
'Component view not found,please check the route configuration',
);
return undefined;
}
const routeName = route.name as string;
// 如果组件没有 name,则直接返回
if (!routeName) {
return component;
}
const componentName = (component?.type as any)?.name;
// 已经设置过 name,则直接返回
if (componentName) {
return component;
}
// componentName 与 routeName 一致,则直接返回
if (componentName === routeName) {
return component;
}
// 设置 name
component.type ||= {};
(component.type as any).name = routeName;
return component;
}
/**
* Layout相关hook
*/
export function useLayoutHook() {
const { keepAlive } = usePreferences();
/**
* 使
*/
const getEnabledTransition = computed(() => {
const { transition } = preferences;
const transitionName = transition.name;
return transitionName && transition.enable;
});
/**
*
* @param _route
*/
function getTransitionName(_route: RouteLocationNormalizedLoaded) {
// 如果偏好设置未设置,则不使用动画
const { tabbar, transition } = preferences;
const transitionName = transition.name;
if (!transitionName || !transition.enable) {
return;
}
// 标签页未启用或者未开启缓存,则使用全局配置动画
if (!tabbar.enable || !keepAlive) {
return transitionName;
}
// 如果页面已经加载过,则不使用动画
// if (route.meta.loaded) {
// return;
// }
// 已经打开且已经加载过的页面不使用动画
// const inTabs = getCachedTabs.value.includes(route.name as string);
// return inTabs && route.meta.loaded ? undefined : transitionName;
return transitionName;
}
return {
getEnabledTransition,
getTransitionName,
};
}

2
packages/effects/layouts/src/route-cached/index.ts

@ -0,0 +1,2 @@
export { default as RouteCachedPage } from './route-cached-page.vue';
export { default as RouteCachedView } from './route-cached-view.vue';

36
packages/effects/layouts/src/route-cached/route-cached-page.vue

@ -0,0 +1,36 @@
<!-- 本组件用于获取缓存的route并保存到pinia -->
<script setup lang="ts">
import type { VNode } from 'vue';
import type { RouteLocationNormalizedLoadedGeneric } from 'vue-router';
import { watch } from 'vue';
import { useTabbarStore } from '@vben/stores';
interface Props {
component?: VNode;
route: RouteLocationNormalizedLoadedGeneric;
}
/**
* 这是页面缓存组件不做任何的的实际渲染
*/
defineOptions({
render() {
return null;
},
});
const props = defineProps<Props>();
const { addCachedRoute } = useTabbarStore();
watch(
() => props.route,
() => {
if (props.component && props.route.meta.domCached) {
addCachedRoute(props.component, props.route);
}
},
{ immediate: true },
);
</script>

98
packages/effects/layouts/src/route-cached/route-cached-view.vue

@ -0,0 +1,98 @@
<script setup lang="ts">
import { computed, unref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { preferences } from '@vben/preferences';
import { getTabKey, storeToRefs, useTabbarStore } from '@vben/stores';
import { transformComponent, useLayoutHook } from '../hooks';
const route = useRoute();
const tabbarStore = useTabbarStore();
const { getTabs, getCachedRoutes, getExcludeCachedTabs } =
storeToRefs(tabbarStore);
const { removeCachedRoute } = tabbarStore;
const { getEnabledTransition, getTransitionName } = useLayoutHook();
/**
* 是否启用tab
*/
const enableTabbar = computed(() => preferences.tabbar.enable);
const computedCachedRouteKeys = computed(() => {
if (!unref(enableTabbar)) {
return [];
}
return unref(getTabs)
.filter((item) => item.meta.domCached)
.map((item) => getTabKey(item));
});
/**
* 监听缓存路由变化删除不存在的缓存路由
*/
watch(computedCachedRouteKeys, (keys) => {
unref(getCachedRoutes).forEach((item) => {
if (!keys.includes(item.key)) {
removeCachedRoute(item.key);
}
});
});
/**
* 所有缓存的route
*/
const computedCachedRoutes = computed(() => {
if (!unref(enableTabbar)) {
return [];
}
//
const excludeCachedTabKeys = unref(getExcludeCachedTabs);
return [...unref(getCachedRoutes).values()].filter((item) => {
const componentType: any = item.component.type || {};
let componentName = componentType.name;
if (!componentName) {
componentName = item.route.name;
}
return !excludeCachedTabKeys.includes(componentName);
});
});
/**
* 是否显示
*/
const computedShowView = computed(() => unref(computedCachedRoutes).length > 0);
const computedCurrentRouteKey = computed(() => {
return getTabKey(route);
});
</script>
<template>
<template v-if="computedShowView">
<template v-for="item in computedCachedRoutes" :key="item.key">
<Transition
v-if="getEnabledTransition"
appear
mode="out-in"
:name="getTransitionName(item.route)"
>
<component
v-show="item.key === computedCurrentRouteKey"
:is="transformComponent(item.component, item.route)"
/>
</Transition>
<template v-else>
<component
v-show="item.key === computedCurrentRouteKey"
:is="transformComponent(item.component, item.route)"
/>
</template>
</template>
</template>
</template>
<style scoped></style>

36
packages/stores/src/modules/tabbar.ts

@ -1,13 +1,15 @@
import type { ComputedRef } from 'vue'; import type { ComputedRef, VNode } from 'vue';
import type { import type {
RouteLocationNormalized, RouteLocationNormalized,
RouteLocationNormalizedLoaded,
RouteLocationNormalizedLoadedGeneric,
Router, Router,
RouteRecordNormalized, RouteRecordNormalized,
} from 'vue-router'; } from 'vue-router';
import type { TabDefinition } from '@vben-core/typings'; import type { TabDefinition } from '@vben-core/typings';
import { toRaw } from 'vue'; import { markRaw, toRaw } from 'vue';
import { preferences } from '@vben-core/preferences'; import { preferences } from '@vben-core/preferences';
import { import {
@ -20,7 +22,14 @@ import {
import { acceptHMRUpdate, defineStore } from 'pinia'; import { acceptHMRUpdate, defineStore } from 'pinia';
interface RouteCached {
component: VNode;
key: string;
route: RouteLocationNormalizedLoadedGeneric;
}
interface TabbarState { interface TabbarState {
cachedRoutes: Map<string, RouteCached>;
/** /**
* @zh_CN * @zh_CN
*/ */
@ -553,6 +562,25 @@ export const useTabbarStore = defineStore('core-tabbar', {
} }
this.cachedTabs = cacheMap; this.cachedTabs = cacheMap;
}, },
/**
* route
* @param component
* @param route
*/
addCachedRoute(component: VNode, route: RouteLocationNormalizedLoaded) {
const key = getTabKey(route);
if (this.cachedRoutes.has(key)) {
return;
}
this.cachedRoutes.set(key, {
key,
component: markRaw(component),
route: markRaw(route),
});
},
removeCachedRoute(key: string) {
this.cachedRoutes.delete(key);
},
}, },
getters: { getters: {
affixTabs(): TabDefinition[] { affixTabs(): TabDefinition[] {
@ -577,6 +605,9 @@ export const useTabbarStore = defineStore('core-tabbar', {
const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab)); const normalTabs = this.tabs.filter((tab) => !isAffixTab(tab));
return [...this.affixTabs, ...normalTabs].filter(Boolean); return [...this.affixTabs, ...normalTabs].filter(Boolean);
}, },
getCachedRoutes(): Map<string, RouteCached> {
return this.cachedRoutes;
},
}, },
persist: [ persist: [
// tabs不需要保存在localStorage // tabs不需要保存在localStorage
@ -604,6 +635,7 @@ export const useTabbarStore = defineStore('core-tabbar', {
], ],
state: (): TabbarState => ({ state: (): TabbarState => ({
visitHistory: createStack<string>(true, MAX_VISIT_HISTORY), visitHistory: createStack<string>(true, MAX_VISIT_HISTORY),
cachedRoutes: new Map<string, RouteCached>(),
cachedTabs: new Set(), cachedTabs: new Set(),
dragEndIndex: 0, dragEndIndex: 0,
excludeCachedTabs: new Set(), excludeCachedTabs: new Set(),

1
playground/src/router/routes/modules/demos.ts

@ -30,6 +30,7 @@ const routes: RouteRecordRaw[] = [
meta: { meta: {
icon: 'mdi:page-previous-outline', icon: 'mdi:page-previous-outline',
title: $t('demos.access.pageAccess'), title: $t('demos.access.pageAccess'),
domCached: true,
}, },
}, },
{ {

Loading…
Cancel
Save