perf: 懒加载表现优化
- 优化路由首进页面过渡策略,减少遮罩阻塞感 - 为菜单导航增加组件预取,降低首次点击等待 - 缩短页面 loading 遮罩过渡时长
This commit is contained in:
@@ -10,6 +10,39 @@ import { useAuthStore } from '#/store';
|
||||
|
||||
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
|
||||
@@ -22,7 +55,7 @@ function setupCommonGuard(router: Router) {
|
||||
to.meta.loaded = loadedPaths.has(to.path);
|
||||
|
||||
// 页面加载进度条
|
||||
if (!to.meta.loaded && preferences.transition.progress) {
|
||||
if (!to.meta.loaded && shouldUseRouteProgress()) {
|
||||
startProgress();
|
||||
}
|
||||
return true;
|
||||
@@ -34,7 +67,7 @@ function setupCommonGuard(router: Router) {
|
||||
loadedPaths.add(to.path);
|
||||
|
||||
// 关闭页面加载进度条
|
||||
if (preferences.transition.progress) {
|
||||
if (shouldUseRouteProgress()) {
|
||||
stopProgress();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ function onTransitionEnd() {
|
||||
<div
|
||||
:class="
|
||||
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,
|
||||
},
|
||||
|
||||
@@ -1,49 +1,138 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
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() {
|
||||
const spinning = ref(false);
|
||||
const startTime = ref(0);
|
||||
const router = useRouter();
|
||||
const minShowTime = 500; // 最小显示时间
|
||||
const showDelay = 120; // 超过该时长才展示loading,避免快速切换闪烁
|
||||
const minVisibleTime = 180; // 展示后最小可见时长,避免抖动
|
||||
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 = () => {
|
||||
if (!enableLoading.value) {
|
||||
clearShowTimer();
|
||||
|
||||
if (!enableOverlayLoading.value) {
|
||||
clearHideTimer();
|
||||
spinning.value = false;
|
||||
return;
|
||||
}
|
||||
const processTime = performance.now() - startTime.value;
|
||||
if (processTime < minShowTime) {
|
||||
setTimeout(() => {
|
||||
spinning.value = false;
|
||||
}, minShowTime - processTime);
|
||||
} else {
|
||||
spinning.value = false;
|
||||
|
||||
if (!spinning.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) {
|
||||
if (to.meta.loaded || !enableOverlayLoading.value || to.meta.iframeSrc) {
|
||||
onEnd();
|
||||
return true;
|
||||
}
|
||||
|
||||
clearHideTimer();
|
||||
|
||||
if (spinning.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
clearShowTimer();
|
||||
showTimer.value = setTimeout(() => {
|
||||
startTime.value = performance.now();
|
||||
spinning.value = true;
|
||||
showTimer.value = undefined;
|
||||
}, showDelay);
|
||||
return true;
|
||||
});
|
||||
|
||||
// 路由后置守卫
|
||||
router.afterEach((to) => {
|
||||
if (to.meta.loaded || !enableLoading.value || to.meta.iframeSrc) {
|
||||
if (to.meta.loaded || !enableOverlayLoading.value || to.meta.iframeSrc) {
|
||||
return true;
|
||||
}
|
||||
onEnd();
|
||||
return true;
|
||||
});
|
||||
|
||||
networkConnection?.addEventListener?.('change', handleNetworkChange);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
networkConnection?.removeEventListener?.('change', handleNetworkChange);
|
||||
clearShowTimer();
|
||||
clearHideTimer();
|
||||
});
|
||||
|
||||
return { spinning };
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useNavigation } from './use-navigation';
|
||||
|
||||
function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
|
||||
const accessStore = useAccessStore();
|
||||
const { navigation, willOpenedByWindow } = useNavigation();
|
||||
const { navigation, prefetch, willOpenedByWindow } = useNavigation();
|
||||
|
||||
const menus = computed(() => useRootMenus?.value ?? accessStore.accessMenus);
|
||||
|
||||
@@ -87,6 +87,8 @@ function useExtraMenu(useRootMenus?: ComputedRef<MenuRecordRaw[]>) {
|
||||
};
|
||||
|
||||
const handleMenuMouseEnter = (menu: MenuRecordRaw) => {
|
||||
prefetch(menu.path);
|
||||
|
||||
if (!preferences.sidebar.expandOnHover) {
|
||||
const { findMenu } = findRootMenuByPath(menus.value, menu.path);
|
||||
extraMenus.value = findMenu?.children ?? [];
|
||||
|
||||
@@ -7,6 +7,8 @@ import { isHttpUrl, openRouteInNewWindow, openWindow } from '@easyflow/utils';
|
||||
function useNavigation() {
|
||||
const router = useRouter();
|
||||
const routeMetaMap = new Map<string, RouteRecordNormalized>();
|
||||
const prefetchedPaths = new Set<string>();
|
||||
const prefetchingPaths = new Set<string>();
|
||||
|
||||
// 初始化路由映射
|
||||
const initRouteMetaMap = () => {
|
||||
@@ -37,6 +39,51 @@ function useNavigation() {
|
||||
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) => {
|
||||
try {
|
||||
const route = routeMetaMap.get(path);
|
||||
@@ -53,6 +100,7 @@ function useNavigation() {
|
||||
} else if (openInNewWindow) {
|
||||
openRouteInNewWindow(resolveHref(path));
|
||||
} else {
|
||||
prefetch(path);
|
||||
await router.push({
|
||||
path,
|
||||
query,
|
||||
@@ -68,7 +116,7 @@ function useNavigation() {
|
||||
return shouldOpenInNewWindow(path);
|
||||
};
|
||||
|
||||
return { navigation, willOpenedByWindow };
|
||||
return { navigation, prefetch, willOpenedByWindow };
|
||||
}
|
||||
|
||||
export { useNavigation };
|
||||
|
||||
Reference in New Issue
Block a user