perf: 懒加载表现优化
- 优化路由首进页面过渡策略,减少遮罩阻塞感 - 为菜单导航增加组件预取,降低首次点击等待 - 缩短页面 loading 遮罩过渡时长
This commit is contained in:
@@ -10,6 +10,39 @@ import { useAuthStore } from '#/store';
|
|||||||
|
|
||||||
import { generateAccess } from './access';
|
import { generateAccess } from './access';
|
||||||
|
|
||||||
|
interface NetworkConnectionLike {
|
||||||
|
effectiveType?: string;
|
||||||
|
saveData?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSlowNetworkConnection() {
|
||||||
|
if (typeof navigator === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const nav = navigator as Navigator & {
|
||||||
|
connection?: NetworkConnectionLike;
|
||||||
|
mozConnection?: NetworkConnectionLike;
|
||||||
|
webkitConnection?: NetworkConnectionLike;
|
||||||
|
};
|
||||||
|
const connection =
|
||||||
|
nav.connection ?? nav.mozConnection ?? nav.webkitConnection;
|
||||||
|
if (!connection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (connection.saveData) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ['2g', '3g', 'slow-2g'].includes(connection.effectiveType ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseRouteProgress() {
|
||||||
|
if (preferences.transition.progress) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 普通网络下,loading遮罩不显示时自动回退到顶部进度条
|
||||||
|
return preferences.transition.loading && !isSlowNetworkConnection();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用守卫配置
|
* 通用守卫配置
|
||||||
* @param router
|
* @param router
|
||||||
@@ -22,7 +55,7 @@ function setupCommonGuard(router: Router) {
|
|||||||
to.meta.loaded = loadedPaths.has(to.path);
|
to.meta.loaded = loadedPaths.has(to.path);
|
||||||
|
|
||||||
// 页面加载进度条
|
// 页面加载进度条
|
||||||
if (!to.meta.loaded && preferences.transition.progress) {
|
if (!to.meta.loaded && shouldUseRouteProgress()) {
|
||||||
startProgress();
|
startProgress();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -34,7 +67,7 @@ function setupCommonGuard(router: Router) {
|
|||||||
loadedPaths.add(to.path);
|
loadedPaths.add(to.path);
|
||||||
|
|
||||||
// 关闭页面加载进度条
|
// 关闭页面加载进度条
|
||||||
if (preferences.transition.progress) {
|
if (shouldUseRouteProgress()) {
|
||||||
stopProgress();
|
stopProgress();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function onTransitionEnd() {
|
|||||||
<div
|
<div
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'flex-center z-100 bg-overlay-content absolute left-0 top-0 size-full backdrop-blur-sm transition-all duration-500',
|
'flex-center z-100 bg-overlay-content absolute left-0 top-0 size-full backdrop-blur-sm transition-opacity duration-200',
|
||||||
{
|
{
|
||||||
'invisible opacity-0': !showSpinner,
|
'invisible opacity-0': !showSpinner,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,49 +1,138 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { preferences } from '@easyflow/preferences';
|
import { preferences } from '@easyflow/preferences';
|
||||||
|
|
||||||
|
interface NetworkConnectionLike {
|
||||||
|
addEventListener?: (event: 'change', listener: () => void) => void;
|
||||||
|
effectiveType?: string;
|
||||||
|
removeEventListener?: (event: 'change', listener: () => void) => void;
|
||||||
|
saveData?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNetworkConnection() {
|
||||||
|
if (typeof navigator === 'undefined') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const nav = navigator as Navigator & {
|
||||||
|
connection?: NetworkConnectionLike;
|
||||||
|
mozConnection?: NetworkConnectionLike;
|
||||||
|
webkitConnection?: NetworkConnectionLike;
|
||||||
|
};
|
||||||
|
return nav.connection ?? nav.mozConnection ?? nav.webkitConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSlowNetworkConnection() {
|
||||||
|
const connection = getNetworkConnection();
|
||||||
|
if (!connection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (connection.saveData) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ['2g', '3g', 'slow-2g'].includes(connection.effectiveType ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
function useContentSpinner() {
|
function useContentSpinner() {
|
||||||
const spinning = ref(false);
|
const spinning = ref(false);
|
||||||
const startTime = ref(0);
|
const startTime = ref(0);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const minShowTime = 500; // 最小显示时间
|
const showDelay = 120; // 超过该时长才展示loading,避免快速切换闪烁
|
||||||
|
const minVisibleTime = 180; // 展示后最小可见时长,避免抖动
|
||||||
const enableLoading = computed(() => preferences.transition.loading);
|
const enableLoading = computed(() => preferences.transition.loading);
|
||||||
|
const slowNetwork = ref(isSlowNetworkConnection());
|
||||||
|
const enableOverlayLoading = computed(
|
||||||
|
() => enableLoading.value && slowNetwork.value,
|
||||||
|
);
|
||||||
|
const showTimer = ref<ReturnType<typeof setTimeout>>();
|
||||||
|
const hideTimer = ref<ReturnType<typeof setTimeout>>();
|
||||||
|
const networkConnection = getNetworkConnection();
|
||||||
|
|
||||||
|
const handleNetworkChange = () => {
|
||||||
|
slowNetwork.value = isSlowNetworkConnection();
|
||||||
|
if (!slowNetwork.value) {
|
||||||
|
onEnd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function clearShowTimer() {
|
||||||
|
if (!showTimer.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(showTimer.value);
|
||||||
|
showTimer.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHideTimer() {
|
||||||
|
if (!hideTimer.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(hideTimer.value);
|
||||||
|
hideTimer.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// 结束加载动画
|
// 结束加载动画
|
||||||
const onEnd = () => {
|
const onEnd = () => {
|
||||||
if (!enableLoading.value) {
|
clearShowTimer();
|
||||||
|
|
||||||
|
if (!enableOverlayLoading.value) {
|
||||||
|
clearHideTimer();
|
||||||
|
spinning.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const processTime = performance.now() - startTime.value;
|
|
||||||
if (processTime < minShowTime) {
|
if (!spinning.value) {
|
||||||
setTimeout(() => {
|
return;
|
||||||
spinning.value = false;
|
|
||||||
}, minShowTime - processTime);
|
|
||||||
} else {
|
|
||||||
spinning.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visibleTime = performance.now() - startTime.value;
|
||||||
|
const remainTime = Math.max(0, minVisibleTime - visibleTime);
|
||||||
|
clearHideTimer();
|
||||||
|
hideTimer.value = setTimeout(() => {
|
||||||
|
spinning.value = false;
|
||||||
|
hideTimer.value = undefined;
|
||||||
|
}, remainTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 路由前置守卫
|
// 路由前置守卫
|
||||||
router.beforeEach((to) => {
|
router.beforeEach((to) => {
|
||||||
if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) {
|
if (to.meta.loaded || !enableOverlayLoading.value || to.meta.iframeSrc) {
|
||||||
|
onEnd();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearHideTimer();
|
||||||
|
|
||||||
|
if (spinning.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearShowTimer();
|
||||||
|
showTimer.value = setTimeout(() => {
|
||||||
startTime.value = performance.now();
|
startTime.value = performance.now();
|
||||||
spinning.value = true;
|
spinning.value = true;
|
||||||
|
showTimer.value = undefined;
|
||||||
|
}, showDelay);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 路由后置守卫
|
// 路由后置守卫
|
||||||
router.afterEach((to) => {
|
router.afterEach((to) => {
|
||||||
if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) {
|
if (to.meta.loaded || !enableOverlayLoading.value || to.meta.iframeSrc) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
onEnd();
|
onEnd();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
networkConnection?.addEventListener?.('change', handleNetworkChange);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
networkConnection?.removeEventListener?.('change', handleNetworkChange);
|
||||||
|
clearShowTimer();
|
||||||
|
clearHideTimer();
|
||||||
|
});
|
||||||
|
|
||||||
return { spinning };
|
return { spinning };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useNavigation } from './use-navigation';
|
|||||||
|
|
||||||
function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
|
function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const { navigation, willOpenedByWindow } = useNavigation();
|
const { navigation, prefetch, willOpenedByWindow } = useNavigation();
|
||||||
|
|
||||||
const menus = computed(() => useRootMenus?.value ?? accessStore.accessMenus);
|
const menus = computed(() => useRootMenus?.value ?? accessStore.accessMenus);
|
||||||
|
|
||||||
@@ -87,6 +87,8 @@ function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMenuMouseEnter = (menu: MenuRecordRaw) => {
|
const handleMenuMouseEnter = (menu: MenuRecordRaw) => {
|
||||||
|
prefetch(menu.path);
|
||||||
|
|
||||||
if (!preferences.sidebar.expandOnHover) {
|
if (!preferences.sidebar.expandOnHover) {
|
||||||
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
|
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
|
||||||
extraMenus.value = findMenu?.children ?? [];
|
extraMenus.value = findMenu?.children ?? [];
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { isHttpUrl, openRouteInNewWindow, openWindow } from '@easyflow/utils';
|
|||||||
function useNavigation() {
|
function useNavigation() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const routeMetaMap = new Map<string, RouteRecordNormalized>();
|
const routeMetaMap = new Map<string, RouteRecordNormalized>();
|
||||||
|
const prefetchedPaths = new Set<string>();
|
||||||
|
const prefetchingPaths = new Set<string>();
|
||||||
|
|
||||||
// 初始化路由映射
|
// 初始化路由映射
|
||||||
const initRouteMetaMap = () => {
|
const initRouteMetaMap = () => {
|
||||||
@@ -37,6 +39,51 @@ function useNavigation() {
|
|||||||
return router.resolve(path).href;
|
return router.resolve(path).href;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const prefetch = (path: string) => {
|
||||||
|
if (
|
||||||
|
isHttpUrl(path) ||
|
||||||
|
prefetchedPaths.has(path) ||
|
||||||
|
prefetchingPaths.has(path)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { matched } = router.resolve(path);
|
||||||
|
if (matched.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentLoadTasks: Array<Promise<unknown>> = [];
|
||||||
|
matched.forEach((route) => {
|
||||||
|
const component = route.components?.default;
|
||||||
|
if (typeof component !== 'function' || component.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loaded = (component as () => unknown)();
|
||||||
|
if (loaded && typeof (loaded as Promise<unknown>).then === 'function') {
|
||||||
|
componentLoadTasks.push(loaded as Promise<unknown>);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 预取失败不影响正常导航
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (componentLoadTasks.length === 0) {
|
||||||
|
prefetchedPaths.add(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefetchingPaths.add(path);
|
||||||
|
void Promise.allSettled(componentLoadTasks)
|
||||||
|
.then(() => {
|
||||||
|
prefetchedPaths.add(path);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
prefetchingPaths.delete(path);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const navigation = async (path: string) => {
|
const navigation = async (path: string) => {
|
||||||
try {
|
try {
|
||||||
const route = routeMetaMap.get(path);
|
const route = routeMetaMap.get(path);
|
||||||
@@ -53,6 +100,7 @@ function useNavigation() {
|
|||||||
} else if (openInNewWindow) {
|
} else if (openInNewWindow) {
|
||||||
openRouteInNewWindow(resolveHref(path));
|
openRouteInNewWindow(resolveHref(path));
|
||||||
} else {
|
} else {
|
||||||
|
prefetch(path);
|
||||||
await router.push({
|
await router.push({
|
||||||
path,
|
path,
|
||||||
query,
|
query,
|
||||||
@@ -68,7 +116,7 @@ function useNavigation() {
|
|||||||
return shouldOpenInNewWindow(path);
|
return shouldOpenInNewWindow(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { navigation, willOpenedByWindow };
|
return { navigation, prefetch, willOpenedByWindow };
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useNavigation };
|
export { useNavigation };
|
||||||
|
|||||||
Reference in New Issue
Block a user