mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
fix: warm lazy navigation routes after startup
This commit is contained in:
@@ -9,7 +9,8 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@mui/material'
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
import type { CSSProperties, PointerEvent, ReactNode } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useMatch, useNavigate, useResolvedPath } from 'react-router'
|
||||
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
@@ -28,9 +29,10 @@ interface Props {
|
||||
children: string
|
||||
icon: ReactNode[]
|
||||
sortable?: SortableProps
|
||||
onPreload?: () => Promise<unknown>
|
||||
}
|
||||
export const LayoutItem = (props: Props) => {
|
||||
const { to, children, icon, sortable } = props
|
||||
const { to, children, icon, sortable, onPreload } = props
|
||||
const { verge } = useVerge()
|
||||
const { menu_icon } = verge ?? {}
|
||||
const navCollapsed = verge?.collapse_navbar ?? false
|
||||
@@ -45,9 +47,21 @@ export const LayoutItem = (props: Props) => {
|
||||
sortable ?? {}
|
||||
|
||||
const draggable = Boolean(sortable) && !disabled
|
||||
const dragHandleProps = draggable
|
||||
? { ...(attributes ?? {}), ...(listeners ?? {}) }
|
||||
: undefined
|
||||
const { onPointerDown, ...otherListeners } = draggable
|
||||
? (listeners ?? {})
|
||||
: {}
|
||||
|
||||
const handlePreload = useCallback(() => {
|
||||
void onPreload?.().catch(() => {})
|
||||
}, [onPreload])
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(event: PointerEvent<HTMLDivElement>) => {
|
||||
handlePreload()
|
||||
onPointerDown?.(event)
|
||||
},
|
||||
[handlePreload, onPointerDown],
|
||||
)
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -60,7 +74,8 @@ export const LayoutItem = (props: Props) => {
|
||||
>
|
||||
<ListItemButton
|
||||
selected={!!match}
|
||||
{...(dragHandleProps ?? {})}
|
||||
{...(draggable ? (attributes ?? {}) : {})}
|
||||
{...(draggable ? otherListeners : {})}
|
||||
sx={[
|
||||
{
|
||||
borderRadius: 2,
|
||||
@@ -90,6 +105,9 @@ export const LayoutItem = (props: Props) => {
|
||||
]}
|
||||
title={navCollapsed ? children : undefined}
|
||||
aria-label={navCollapsed ? children : undefined}
|
||||
onFocus={handlePreload}
|
||||
onMouseEnter={handlePreload}
|
||||
onPointerDown={handlePointerDown}
|
||||
onClick={() => navigate(to)}
|
||||
>
|
||||
{(effectiveMenuIcon === 'monochrome' || !effectiveMenuIcon) && (
|
||||
|
||||
+25
-7
@@ -48,7 +48,6 @@ import { WindowControls } from '@/components/layout/window-controller'
|
||||
import { useI18n } from '@/hooks/use-i18n'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { useWindowDecorations } from '@/hooks/use-window'
|
||||
import { ensureLanguageSections } from '@/services/i18n'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
@@ -59,17 +58,14 @@ import {
|
||||
useNavMenuOrder,
|
||||
} from './_layout/hooks'
|
||||
import { handleNoticeMessage } from './_layout/utils'
|
||||
import { navItems } from './_routers'
|
||||
import { navItems, preloadLogsPage, preloadNavigationRoutes } from './_routers'
|
||||
|
||||
import 'dayjs/locale/ru'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
export const portableFlag = false
|
||||
|
||||
const LogsPage = lazy(async () => {
|
||||
await ensureLanguageSections('logs')
|
||||
return import('./logs')
|
||||
})
|
||||
const LogsPage = lazy(() => preloadLogsPage())
|
||||
|
||||
type NavItem = (typeof navItems)[number]
|
||||
|
||||
@@ -105,6 +101,7 @@ const SortableNavMenuItem = ({ item, label }: SortableNavMenuItemProps) => {
|
||||
<LayoutItem
|
||||
to={item.path}
|
||||
icon={item.icon}
|
||||
onPreload={item.preload}
|
||||
sortable={{
|
||||
setNodeRef,
|
||||
attributes,
|
||||
@@ -232,6 +229,22 @@ const Layout = () => {
|
||||
|
||||
useLoadingOverlay(themeReady)
|
||||
|
||||
useEffect(() => {
|
||||
if (!themeReady) {
|
||||
return
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const timerId = window.setTimeout(() => {
|
||||
void preloadNavigationRoutes(controller.signal)
|
||||
}, 2000)
|
||||
|
||||
return () => {
|
||||
controller.abort()
|
||||
window.clearTimeout(timerId)
|
||||
}
|
||||
}, [themeReady])
|
||||
|
||||
const handleNotice = useCallback(
|
||||
(payload: [string, string]) => {
|
||||
const [status, msg] = payload
|
||||
@@ -406,7 +419,12 @@ const Layout = () => {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<LayoutItem key={item.path} to={item.path} icon={item.icon}>
|
||||
<LayoutItem
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
icon={item.icon}
|
||||
onPreload={item.preload}
|
||||
>
|
||||
{t(item.label)}
|
||||
</LayoutItem>
|
||||
)
|
||||
|
||||
+88
-12
@@ -22,27 +22,83 @@ import { ensureLanguageSections } from '@/services/i18n'
|
||||
import Layout from './_layout'
|
||||
import HomePage from './home'
|
||||
|
||||
const createLazyRoute = (
|
||||
const waitForWarmupIdle = (signal: AbortSignal) =>
|
||||
new Promise<void>((resolve) => {
|
||||
let idleId: number | undefined
|
||||
let timeoutId: number | undefined
|
||||
|
||||
const cleanup = () => {
|
||||
signal.removeEventListener('abort', finish)
|
||||
if (idleId !== undefined) {
|
||||
window.cancelIdleCallback(idleId)
|
||||
}
|
||||
if (timeoutId !== undefined) {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
const finish = () => {
|
||||
cleanup()
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
signal.addEventListener('abort', finish, { once: true })
|
||||
|
||||
if (window.requestIdleCallback) {
|
||||
idleId = window.requestIdleCallback(finish, { timeout: 500 })
|
||||
} else {
|
||||
timeoutId = window.setTimeout(finish, 120)
|
||||
}
|
||||
})
|
||||
|
||||
const createRoutePreload = (
|
||||
load: () => Promise<{ default: ComponentType }>,
|
||||
sections?: string | readonly string[],
|
||||
) => {
|
||||
const Component = lazy(
|
||||
sections
|
||||
let preloadPromise: Promise<{ default: ComponentType }> | undefined
|
||||
|
||||
const startPreload = sections
|
||||
? async () => {
|
||||
await ensureLanguageSections(sections)
|
||||
return load()
|
||||
}
|
||||
: load,
|
||||
)
|
||||
: load
|
||||
|
||||
return () => {
|
||||
preloadPromise ??= startPreload().catch((error) => {
|
||||
preloadPromise = undefined
|
||||
throw error
|
||||
})
|
||||
|
||||
return preloadPromise
|
||||
}
|
||||
}
|
||||
|
||||
const createLazyRoute = (
|
||||
load: () => Promise<{ default: ComponentType }>,
|
||||
sections?: string | readonly string[],
|
||||
) => {
|
||||
const preload = createRoutePreload(load, sections)
|
||||
const Component = lazy(preload)
|
||||
const LazyRoute = () => (
|
||||
<Suspense fallback={null}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
return LazyRoute
|
||||
return { Component: LazyRoute, preload }
|
||||
}
|
||||
|
||||
export const preloadLogsPage = createRoutePreload(
|
||||
() => import('./logs'),
|
||||
'logs',
|
||||
)
|
||||
|
||||
export const navItems = [
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.home',
|
||||
@@ -54,46 +110,66 @@ export const navItems = [
|
||||
label: 'layout.components.navigation.tabs.proxies',
|
||||
path: '/proxies',
|
||||
icon: [<WifiRoundedIcon key="mui" />, <ProxiesSvg key="svg" />],
|
||||
Component: createLazyRoute(() => import('./proxies')),
|
||||
...createLazyRoute(() => import('./proxies')),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.profiles',
|
||||
path: '/profile',
|
||||
icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
|
||||
Component: createLazyRoute(() => import('./profiles'), 'rules'),
|
||||
...createLazyRoute(() => import('./profiles'), 'rules'),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.connections',
|
||||
path: '/connections',
|
||||
icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
|
||||
Component: createLazyRoute(() => import('./connections'), 'connections'),
|
||||
...createLazyRoute(() => import('./connections'), 'connections'),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.rules',
|
||||
path: '/rules',
|
||||
icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
|
||||
Component: createLazyRoute(() => import('./rules'), 'rules'),
|
||||
...createLazyRoute(() => import('./rules'), 'rules'),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.logs',
|
||||
path: '/logs',
|
||||
icon: [<SubjectRoundedIcon key="mui" />, <LogsSvg key="svg" />],
|
||||
Component: () => null /* LogsPage rendered in Layout only on /logs route */,
|
||||
preload: preloadLogsPage,
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.unlock',
|
||||
path: '/unlock',
|
||||
icon: [<LockOpenRoundedIcon key="mui" />, <UnlockSvg key="svg" />],
|
||||
Component: createLazyRoute(() => import('./unlock')),
|
||||
...createLazyRoute(() => import('./unlock')),
|
||||
},
|
||||
{
|
||||
label: 'layout.components.navigation.tabs.settings',
|
||||
path: '/settings',
|
||||
icon: [<SettingsRoundedIcon key="mui" />, <SettingsSvg key="svg" />],
|
||||
Component: createLazyRoute(() => import('./settings')),
|
||||
...createLazyRoute(() => import('./settings')),
|
||||
},
|
||||
]
|
||||
|
||||
export const preloadNavigationRoutes = async (signal: AbortSignal) => {
|
||||
for (const item of navItems) {
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
const preload = 'preload' in item ? item.preload : undefined
|
||||
if (!preload) {
|
||||
continue
|
||||
}
|
||||
|
||||
await waitForWarmupIdle(signal)
|
||||
if (signal.aborted) {
|
||||
return
|
||||
}
|
||||
|
||||
await preload().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
|
||||
Reference in New Issue
Block a user