perf: 页面懒加载交互体验优化

This commit is contained in:
2026-02-28 20:51:11 +08:00
parent 01f354ede5
commit 59c95a3b06
3 changed files with 79 additions and 27 deletions

View File

@@ -15,6 +15,8 @@ interface NetworkConnectionLike {
saveData?: boolean;
}
const CHUNK_ERROR_RELOAD_KEY = '__easyflow_chunk_error_reload_path__';
function isSlowNetworkConnection() {
if (typeof navigator === 'undefined') {
return false;
@@ -35,6 +37,46 @@ function isSlowNetworkConnection() {
return ['2g', '3g', 'slow-2g'].includes(connection.effectiveType ?? '');
}
function isDynamicImportChunkError(error: unknown) {
const message = String((error as Error | undefined)?.message ?? error ?? '');
if (!message) {
return false;
}
return (
/Loading chunk/i.test(message) ||
/Importing a module script failed/i.test(message) ||
/Failed to fetch dynamically imported module/i.test(message) ||
/dynamically imported module/i.test(message)
);
}
function setupChunkErrorGuard(router: Router) {
router.onError((error, to) => {
if (!isDynamicImportChunkError(error)) {
return;
}
const fullPath = to?.fullPath;
if (!fullPath) {
return;
}
const lastReloadPath = sessionStorage.getItem(CHUNK_ERROR_RELOAD_KEY);
if (lastReloadPath === fullPath) {
sessionStorage.removeItem(CHUNK_ERROR_RELOAD_KEY);
return;
}
sessionStorage.setItem(CHUNK_ERROR_RELOAD_KEY, fullPath);
window.location.assign(fullPath);
});
router.isReady().finally(() => {
const reloadedPath = sessionStorage.getItem(CHUNK_ERROR_RELOAD_KEY);
if (reloadedPath && router.currentRoute.value.fullPath === reloadedPath) {
sessionStorage.removeItem(CHUNK_ERROR_RELOAD_KEY);
}
});
}
function shouldUseRouteProgress() {
if (preferences.transition.progress) {
return true;
@@ -61,10 +103,11 @@ function setupCommonGuard(router: Router) {
return true;
});
router.afterEach((to) => {
router.afterEach((to, _from, failure) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
if (!failure) {
loadedPaths.add(to.path);
}
// 关闭页面加载进度条
if (shouldUseRouteProgress()) {
@@ -161,6 +204,8 @@ function createRouterGuard(router: Router) {
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
/** chunk加载错误兜底 */
setupChunkErrorGuard(router);
}
export { createRouterGuard };

View File

@@ -1,11 +1,10 @@
<script lang="ts" setup>
import type {VNode} from 'vue';
import {computed} from 'vue';
import type {
RouteLocationNormalizedLoaded,
RouteLocationNormalizedLoadedGeneric,
} from 'vue-router';
import { computed } from 'vue';
import {RouterView} from 'vue-router';
import {preferences, usePreferences} from '@easyflow/preferences';
@@ -104,7 +103,6 @@ function transformComponent(
v-if="getEnabledTransition"
:name="getTransitionName(route)"
appear
mode="out-in"
>
<KeepAlive
v-if="keepAlive"

View File

@@ -10,7 +10,7 @@ import { findRootMenuByPath } from '@easyflow/utils';
import {useNavigation} from './use-navigation';
function useMixedMenu() {
const { navigation, willOpenedByWindow } = useNavigation();
const { navigation, prefetch, willOpenedByWindow } = useNavigation();
const accessStore = useAccessStore();
const route = useRoute();
const splitSideMenus = ref<MenuRecordRaw[]>([]);
@@ -85,6 +85,8 @@ function useMixedMenu() {
* @param mode 菜单模式
*/
const handleMenuSelect = (key: string, mode?: string) => {
prefetch(key);
if (!needSplit.value || mode === 'vertical') {
navigation(key);
return;
@@ -95,6 +97,11 @@ function useMixedMenu() {
if (!willOpenedByWindow(key)) {
rootMenuPath.value = rootMenu?.path ?? '';
splitSideMenus.value = _splitSideMenus;
_splitSideMenus.forEach((menu) => {
if (menu.path) {
prefetch(menu.path);
}
});
}
if (_splitSideMenus.length === 0) {
@@ -114,6 +121,8 @@ function useMixedMenu() {
* @param parentsPath 父级路径
*/
const handleMenuOpen = (key: string, parentsPath: string[]) => {
prefetch(key);
if (parentsPath.length <= 1 && preferences.sidebar.autoActivateChild) {
navigation(
defaultSubMap.has(key) ? (defaultSubMap.get(key) as string) : key,