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,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
} from '@mui/material'
|
} 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 { useMatch, useNavigate, useResolvedPath } from 'react-router'
|
||||||
|
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
@@ -28,9 +29,10 @@ interface Props {
|
|||||||
children: string
|
children: string
|
||||||
icon: ReactNode[]
|
icon: ReactNode[]
|
||||||
sortable?: SortableProps
|
sortable?: SortableProps
|
||||||
|
onPreload?: () => Promise<unknown>
|
||||||
}
|
}
|
||||||
export const LayoutItem = (props: Props) => {
|
export const LayoutItem = (props: Props) => {
|
||||||
const { to, children, icon, sortable } = props
|
const { to, children, icon, sortable, onPreload } = props
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
const { menu_icon } = verge ?? {}
|
const { menu_icon } = verge ?? {}
|
||||||
const navCollapsed = verge?.collapse_navbar ?? false
|
const navCollapsed = verge?.collapse_navbar ?? false
|
||||||
@@ -45,9 +47,21 @@ export const LayoutItem = (props: Props) => {
|
|||||||
sortable ?? {}
|
sortable ?? {}
|
||||||
|
|
||||||
const draggable = Boolean(sortable) && !disabled
|
const draggable = Boolean(sortable) && !disabled
|
||||||
const dragHandleProps = draggable
|
const { onPointerDown, ...otherListeners } = draggable
|
||||||
? { ...(attributes ?? {}), ...(listeners ?? {}) }
|
? (listeners ?? {})
|
||||||
: undefined
|
: {}
|
||||||
|
|
||||||
|
const handlePreload = useCallback(() => {
|
||||||
|
void onPreload?.().catch(() => {})
|
||||||
|
}, [onPreload])
|
||||||
|
|
||||||
|
const handlePointerDown = useCallback(
|
||||||
|
(event: PointerEvent<HTMLDivElement>) => {
|
||||||
|
handlePreload()
|
||||||
|
onPointerDown?.(event)
|
||||||
|
},
|
||||||
|
[handlePreload, onPointerDown],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
@@ -60,7 +74,8 @@ export const LayoutItem = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
selected={!!match}
|
selected={!!match}
|
||||||
{...(dragHandleProps ?? {})}
|
{...(draggable ? (attributes ?? {}) : {})}
|
||||||
|
{...(draggable ? otherListeners : {})}
|
||||||
sx={[
|
sx={[
|
||||||
{
|
{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
@@ -90,6 +105,9 @@ export const LayoutItem = (props: Props) => {
|
|||||||
]}
|
]}
|
||||||
title={navCollapsed ? children : undefined}
|
title={navCollapsed ? children : undefined}
|
||||||
aria-label={navCollapsed ? children : undefined}
|
aria-label={navCollapsed ? children : undefined}
|
||||||
|
onFocus={handlePreload}
|
||||||
|
onMouseEnter={handlePreload}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
onClick={() => navigate(to)}
|
onClick={() => navigate(to)}
|
||||||
>
|
>
|
||||||
{(effectiveMenuIcon === 'monochrome' || !effectiveMenuIcon) && (
|
{(effectiveMenuIcon === 'monochrome' || !effectiveMenuIcon) && (
|
||||||
|
|||||||
+25
-7
@@ -48,7 +48,6 @@ import { WindowControls } from '@/components/layout/window-controller'
|
|||||||
import { useI18n } from '@/hooks/use-i18n'
|
import { useI18n } from '@/hooks/use-i18n'
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { useWindowDecorations } from '@/hooks/use-window'
|
import { useWindowDecorations } from '@/hooks/use-window'
|
||||||
import { ensureLanguageSections } from '@/services/i18n'
|
|
||||||
import { useThemeMode } from '@/services/states'
|
import { useThemeMode } from '@/services/states'
|
||||||
import getSystem from '@/utils/get-system'
|
import getSystem from '@/utils/get-system'
|
||||||
|
|
||||||
@@ -59,17 +58,14 @@ import {
|
|||||||
useNavMenuOrder,
|
useNavMenuOrder,
|
||||||
} from './_layout/hooks'
|
} from './_layout/hooks'
|
||||||
import { handleNoticeMessage } from './_layout/utils'
|
import { handleNoticeMessage } from './_layout/utils'
|
||||||
import { navItems } from './_routers'
|
import { navItems, preloadLogsPage, preloadNavigationRoutes } from './_routers'
|
||||||
|
|
||||||
import 'dayjs/locale/ru'
|
import 'dayjs/locale/ru'
|
||||||
import 'dayjs/locale/zh-cn'
|
import 'dayjs/locale/zh-cn'
|
||||||
|
|
||||||
export const portableFlag = false
|
export const portableFlag = false
|
||||||
|
|
||||||
const LogsPage = lazy(async () => {
|
const LogsPage = lazy(() => preloadLogsPage())
|
||||||
await ensureLanguageSections('logs')
|
|
||||||
return import('./logs')
|
|
||||||
})
|
|
||||||
|
|
||||||
type NavItem = (typeof navItems)[number]
|
type NavItem = (typeof navItems)[number]
|
||||||
|
|
||||||
@@ -105,6 +101,7 @@ const SortableNavMenuItem = ({ item, label }: SortableNavMenuItemProps) => {
|
|||||||
<LayoutItem
|
<LayoutItem
|
||||||
to={item.path}
|
to={item.path}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
|
onPreload={item.preload}
|
||||||
sortable={{
|
sortable={{
|
||||||
setNodeRef,
|
setNodeRef,
|
||||||
attributes,
|
attributes,
|
||||||
@@ -232,6 +229,22 @@ const Layout = () => {
|
|||||||
|
|
||||||
useLoadingOverlay(themeReady)
|
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(
|
const handleNotice = useCallback(
|
||||||
(payload: [string, string]) => {
|
(payload: [string, string]) => {
|
||||||
const [status, msg] = payload
|
const [status, msg] = payload
|
||||||
@@ -406,7 +419,12 @@ const Layout = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
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)}
|
{t(item.label)}
|
||||||
</LayoutItem>
|
</LayoutItem>
|
||||||
)
|
)
|
||||||
|
|||||||
+91
-15
@@ -22,27 +22,83 @@ import { ensureLanguageSections } from '@/services/i18n'
|
|||||||
import Layout from './_layout'
|
import Layout from './_layout'
|
||||||
import HomePage from './home'
|
import HomePage from './home'
|
||||||
|
|
||||||
|
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[],
|
||||||
|
) => {
|
||||||
|
let preloadPromise: Promise<{ default: ComponentType }> | undefined
|
||||||
|
|
||||||
|
const startPreload = sections
|
||||||
|
? async () => {
|
||||||
|
await ensureLanguageSections(sections)
|
||||||
|
return load()
|
||||||
|
}
|
||||||
|
: load
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
preloadPromise ??= startPreload().catch((error) => {
|
||||||
|
preloadPromise = undefined
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
|
||||||
|
return preloadPromise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createLazyRoute = (
|
const createLazyRoute = (
|
||||||
load: () => Promise<{ default: ComponentType }>,
|
load: () => Promise<{ default: ComponentType }>,
|
||||||
sections?: string | readonly string[],
|
sections?: string | readonly string[],
|
||||||
) => {
|
) => {
|
||||||
const Component = lazy(
|
const preload = createRoutePreload(load, sections)
|
||||||
sections
|
const Component = lazy(preload)
|
||||||
? async () => {
|
|
||||||
await ensureLanguageSections(sections)
|
|
||||||
return load()
|
|
||||||
}
|
|
||||||
: load,
|
|
||||||
)
|
|
||||||
const LazyRoute = () => (
|
const LazyRoute = () => (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Component />
|
<Component />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
|
|
||||||
return LazyRoute
|
return { Component: LazyRoute, preload }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const preloadLogsPage = createRoutePreload(
|
||||||
|
() => import('./logs'),
|
||||||
|
'logs',
|
||||||
|
)
|
||||||
|
|
||||||
export const navItems = [
|
export const navItems = [
|
||||||
{
|
{
|
||||||
label: 'layout.components.navigation.tabs.home',
|
label: 'layout.components.navigation.tabs.home',
|
||||||
@@ -54,46 +110,66 @@ export const navItems = [
|
|||||||
label: 'layout.components.navigation.tabs.proxies',
|
label: 'layout.components.navigation.tabs.proxies',
|
||||||
path: '/proxies',
|
path: '/proxies',
|
||||||
icon: [<WifiRoundedIcon key="mui" />, <ProxiesSvg key="svg" />],
|
icon: [<WifiRoundedIcon key="mui" />, <ProxiesSvg key="svg" />],
|
||||||
Component: createLazyRoute(() => import('./proxies')),
|
...createLazyRoute(() => import('./proxies')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'layout.components.navigation.tabs.profiles',
|
label: 'layout.components.navigation.tabs.profiles',
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
|
icon: [<DnsRoundedIcon key="mui" />, <ProfilesSvg key="svg" />],
|
||||||
Component: createLazyRoute(() => import('./profiles'), 'rules'),
|
...createLazyRoute(() => import('./profiles'), 'rules'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'layout.components.navigation.tabs.connections',
|
label: 'layout.components.navigation.tabs.connections',
|
||||||
path: '/connections',
|
path: '/connections',
|
||||||
icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
|
icon: [<LanguageRoundedIcon key="mui" />, <ConnectionsSvg key="svg" />],
|
||||||
Component: createLazyRoute(() => import('./connections'), 'connections'),
|
...createLazyRoute(() => import('./connections'), 'connections'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'layout.components.navigation.tabs.rules',
|
label: 'layout.components.navigation.tabs.rules',
|
||||||
path: '/rules',
|
path: '/rules',
|
||||||
icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
|
icon: [<ForkRightRoundedIcon key="mui" />, <RulesSvg key="svg" />],
|
||||||
Component: createLazyRoute(() => import('./rules'), 'rules'),
|
...createLazyRoute(() => import('./rules'), 'rules'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'layout.components.navigation.tabs.logs',
|
label: 'layout.components.navigation.tabs.logs',
|
||||||
path: '/logs',
|
path: '/logs',
|
||||||
icon: [<SubjectRoundedIcon key="mui" />, <LogsSvg key="svg" />],
|
icon: [<SubjectRoundedIcon key="mui" />, <LogsSvg key="svg" />],
|
||||||
Component: () => null /* LogsPage rendered in Layout only on /logs route */,
|
Component: () => null /* LogsPage rendered in Layout only on /logs route */,
|
||||||
|
preload: preloadLogsPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'layout.components.navigation.tabs.unlock',
|
label: 'layout.components.navigation.tabs.unlock',
|
||||||
path: '/unlock',
|
path: '/unlock',
|
||||||
icon: [<LockOpenRoundedIcon key="mui" />, <UnlockSvg key="svg" />],
|
icon: [<LockOpenRoundedIcon key="mui" />, <UnlockSvg key="svg" />],
|
||||||
Component: createLazyRoute(() => import('./unlock')),
|
...createLazyRoute(() => import('./unlock')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'layout.components.navigation.tabs.settings',
|
label: 'layout.components.navigation.tabs.settings',
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
icon: [<SettingsRoundedIcon key="mui" />, <SettingsSvg key="svg" />],
|
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([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
Reference in New Issue
Block a user