Files
sub2api/frontend/src/composables/useRoutePrefetch.ts

203 lines
5.6 KiB
TypeScript
Raw Normal View History

/**
*
* 访
*
*
* - 使 import()
* - import
* -
*/
import { ref, readonly } from 'vue'
import type { RouteLocationNormalized, Router } from 'vue-router'
/**
*
*/
type ComponentImportFn = () => Promise<unknown>
/**
*
* import
*/
const PREFETCH_ADJACENCY: Record<string, string[]> = {
// Admin routes - 预加载最常访问的相邻页面
'/admin/dashboard': ['/admin/accounts', '/admin/users'],
'/admin/accounts': ['/admin/dashboard', '/admin/users'],
'/admin/users': ['/admin/groups', '/admin/dashboard'],
'/admin/groups': ['/admin/subscriptions', '/admin/users'],
'/admin/subscriptions': ['/admin/groups', '/admin/redeem'],
// User routes
'/dashboard': ['/keys', '/usage'],
'/keys': ['/dashboard', '/usage'],
'/usage': ['/keys', '/redeem'],
'/redeem': ['/usage', '/profile'],
'/profile': ['/dashboard', '/keys']
}
/**
* requestIdleCallback
*/
type IdleCallbackHandle = number | ReturnType<typeof setTimeout>
/**
* requestIdleCallback polyfill (Safari < 15)
*/
const scheduleIdleCallback = (
callback: IdleRequestCallback,
options?: IdleRequestOptions
): IdleCallbackHandle => {
if (typeof window.requestIdleCallback === 'function') {
return window.requestIdleCallback(callback, options)
}
return setTimeout(() => {
callback({ didTimeout: false, timeRemaining: () => 50 })
}, 1000)
}
const cancelScheduledCallback = (handle: IdleCallbackHandle): void => {
if (typeof window.cancelIdleCallback === 'function' && typeof handle === 'number') {
window.cancelIdleCallback(handle)
} else {
clearTimeout(handle)
}
}
/**
*
*
* @param router - Vue Router
*/
export function useRoutePrefetch(router?: Router) {
// 当前挂起的预加载任务句柄
const pendingPrefetchHandle = ref<IdleCallbackHandle | null>(null)
// 已预加载的路由集合
const prefetchedRoutes = ref<Set<string>>(new Set())
/**
* import
*/
const getComponentImporter = (path: string): ComponentImportFn | null => {
if (!router) return null
const routes = router.getRoutes()
const route = routes.find((r) => r.path === path)
if (route && route.components?.default) {
const component = route.components.default
// 检查是否是懒加载组件(函数形式)
if (typeof component === 'function') {
return component as ComponentImportFn
}
}
return null
}
/**
*
*/
const getPrefetchPaths = (route: RouteLocationNormalized): string[] => {
return PREFETCH_ADJACENCY[route.path] || []
}
/**
*
*/
const prefetchComponent = async (importFn: ComponentImportFn): Promise<void> => {
try {
await importFn()
} catch (error) {
// 静默处理预加载错误
if (import.meta.env.DEV) {
console.debug('[Prefetch] Failed to prefetch component:', error)
}
}
}
/**
*
*/
const cancelPendingPrefetch = (): void => {
if (pendingPrefetchHandle.value !== null) {
cancelScheduledCallback(pendingPrefetchHandle.value)
pendingPrefetchHandle.value = null
}
}
/**
*
*/
const triggerPrefetch = (route: RouteLocationNormalized): void => {
cancelPendingPrefetch()
const prefetchPaths = getPrefetchPaths(route)
if (prefetchPaths.length === 0) return
pendingPrefetchHandle.value = scheduleIdleCallback(
() => {
pendingPrefetchHandle.value = null
const routePath = route.path
if (prefetchedRoutes.value.has(routePath)) return
// 获取需要预加载的组件 import 函数
const importFns: ComponentImportFn[] = []
for (const path of prefetchPaths) {
const importFn = getComponentImporter(path)
if (importFn) {
importFns.push(importFn)
}
}
if (importFns.length > 0) {
Promise.all(importFns.map(prefetchComponent)).then(() => {
prefetchedRoutes.value.add(routePath)
})
}
},
{ timeout: 2000 }
)
}
/**
*
*/
const resetPrefetchState = (): void => {
cancelPendingPrefetch()
prefetchedRoutes.value.clear()
}
/**
*
*/
const isAdminRoute = (path: string): boolean => {
return path.startsWith('/admin')
}
/**
* API
*/
const getPrefetchConfig = (route: RouteLocationNormalized): ComponentImportFn[] => {
const paths = getPrefetchPaths(route)
const importFns: ComponentImportFn[] = []
for (const path of paths) {
const importFn = getComponentImporter(path)
if (importFn) importFns.push(importFn)
}
return importFns
}
return {
prefetchedRoutes: readonly(prefetchedRoutes),
triggerPrefetch,
cancelPendingPrefetch,
resetPrefetchState,
_getPrefetchConfig: getPrefetchConfig,
_isAdminRoute: isAdminRoute
}
}
// 兼容旧测试的导出
export const _adminPrefetchMap = PREFETCH_ADJACENCY
export const _userPrefetchMap = PREFETCH_ADJACENCY