perf: 页面懒加载交互体验优化
This commit is contained in:
@@ -1,20 +1,22 @@
|
||||
import type { Router } from 'vue-router';
|
||||
import type {Router} from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@easyflow/constants';
|
||||
import { preferences } from '@easyflow/preferences';
|
||||
import { useAccessStore, useUserStore } from '@easyflow/stores';
|
||||
import { startProgress, stopProgress } from '@easyflow/utils';
|
||||
import {LOGIN_PATH} from '@easyflow/constants';
|
||||
import {preferences} from '@easyflow/preferences';
|
||||
import {useAccessStore, useUserStore} from '@easyflow/stores';
|
||||
import {startProgress, stopProgress} from '@easyflow/utils';
|
||||
|
||||
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
||||
import { useAuthStore } from '#/store';
|
||||
import {accessRoutes, coreRouteNames} from '#/router/routes';
|
||||
import {useAuthStore} from '#/store';
|
||||
|
||||
import { generateAccess } from './access';
|
||||
import {generateAccess} from './access';
|
||||
|
||||
interface NetworkConnectionLike {
|
||||
effectiveType?: string;
|
||||
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 };
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VNode } from 'vue';
|
||||
import type {VNode} from 'vue';
|
||||
import {computed} from 'vue';
|
||||
import type {
|
||||
RouteLocationNormalizedLoaded,
|
||||
RouteLocationNormalizedLoadedGeneric,
|
||||
} from 'vue-router';
|
||||
import {RouterView} from 'vue-router';
|
||||
|
||||
import { computed } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
import {preferences, usePreferences} from '@easyflow/preferences';
|
||||
import {getTabKey, storeToRefs, useTabbarStore} from '@easyflow/stores';
|
||||
|
||||
import { preferences, usePreferences } from '@easyflow/preferences';
|
||||
import { getTabKey, storeToRefs, useTabbarStore } from '@easyflow/stores';
|
||||
|
||||
import { IFrameRouterView } from '../../iframe';
|
||||
import {IFrameRouterView} from '../../iframe';
|
||||
|
||||
defineOptions({ name: 'LayoutContent' });
|
||||
|
||||
@@ -104,7 +103,6 @@ function transformComponent(
|
||||
v-if="getEnabledTransition"
|
||||
:name="getTransitionName(route)"
|
||||
appear
|
||||
mode="out-in"
|
||||
>
|
||||
<KeepAlive
|
||||
v-if="keepAlive"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { MenuRecordRaw } from '@easyflow/types';
|
||||
import type {MenuRecordRaw} from '@easyflow/types';
|
||||
|
||||
import { computed, onBeforeMount, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import {computed, onBeforeMount, ref, watch} from 'vue';
|
||||
import {useRoute} from 'vue-router';
|
||||
|
||||
import { preferences, usePreferences } from '@easyflow/preferences';
|
||||
import { useAccessStore } from '@easyflow/stores';
|
||||
import { findRootMenuByPath } from '@easyflow/utils';
|
||||
import {preferences, usePreferences} from '@easyflow/preferences';
|
||||
import {useAccessStore} from '@easyflow/stores';
|
||||
import {findRootMenuByPath} from '@easyflow/utils';
|
||||
|
||||
import { useNavigation } from './use-navigation';
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user