2026-01-16 21:43:39 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 路由预加载组合式函数
|
|
|
|
|
|
* 在浏览器空闲时预加载可能访问的下一个页面,提升导航体验
|
2026-01-16 22:07:39 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 优化说明:
|
|
|
|
|
|
* - 不使用静态 import() 映射表,避免增加入口文件大小
|
|
|
|
|
|
* - 通过路由配置动态获取组件的 import 函数
|
|
|
|
|
|
* - 只在实际需要预加载时才执行
|
2026-01-16 21:43:39 +08:00
|
|
|
|
*/
|
|
|
|
|
|
import { ref, readonly } from 'vue'
|
2026-01-16 22:07:39 +08:00
|
|
|
|
import type { RouteLocationNormalized, Router } from 'vue-router'
|
2026-01-16 21:43:39 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 组件导入函数类型
|
|
|
|
|
|
*/
|
|
|
|
|
|
type ComponentImportFn = () => Promise<unknown>
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-16 22:07:39 +08:00
|
|
|
|
* 预加载邻接表:定义每个路由应该预加载哪些相邻路由
|
|
|
|
|
|
* 只存储路由路径,不存储 import 函数,避免打包问题
|
2026-01-16 21:43:39 +08:00
|
|
|
|
*/
|
2026-01-16 22:07:39 +08:00
|
|
|
|
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']
|
2026-01-16 21:43:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* requestIdleCallback 的返回类型
|
|
|
|
|
|
*/
|
|
|
|
|
|
type IdleCallbackHandle = number | ReturnType<typeof setTimeout>
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-16 22:07:39 +08:00
|
|
|
|
* requestIdleCallback polyfill (Safari < 15)
|
2026-01-16 21:43:39 +08:00
|
|
|
|
*/
|
|
|
|
|
|
const scheduleIdleCallback = (
|
|
|
|
|
|
callback: IdleRequestCallback,
|
|
|
|
|
|
options?: IdleRequestOptions
|
|
|
|
|
|
): IdleCallbackHandle => {
|
|
|
|
|
|
if (typeof window.requestIdleCallback === 'function') {
|
|
|
|
|
|
return window.requestIdleCallback(callback, options)
|
|
|
|
|
|
}
|
|
|
|
|
|
return setTimeout(() => {
|
2026-01-16 22:07:39 +08:00
|
|
|
|
callback({ didTimeout: false, timeRemaining: () => 50 })
|
2026-01-16 21:43:39 +08:00
|
|
|
|
}, 1000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cancelScheduledCallback = (handle: IdleCallbackHandle): void => {
|
|
|
|
|
|
if (typeof window.cancelIdleCallback === 'function' && typeof handle === 'number') {
|
|
|
|
|
|
window.cancelIdleCallback(handle)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
clearTimeout(handle)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 路由预加载组合式函数
|
|
|
|
|
|
*
|
2026-01-16 22:07:39 +08:00
|
|
|
|
* @param router - Vue Router 实例,用于获取路由组件
|
2026-01-16 21:43:39 +08:00
|
|
|
|
*/
|
2026-01-16 22:07:39 +08:00
|
|
|
|
export function useRoutePrefetch(router?: Router) {
|
2026-01-16 21:43:39 +08:00
|
|
|
|
// 当前挂起的预加载任务句柄
|
|
|
|
|
|
const pendingPrefetchHandle = ref<IdleCallbackHandle | null>(null)
|
|
|
|
|
|
|
2026-01-16 22:07:39 +08:00
|
|
|
|
// 已预加载的路由集合
|
2026-01-16 21:43:39 +08:00
|
|
|
|
const prefetchedRoutes = ref<Set<string>>(new Set())
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-16 22:07:39 +08:00
|
|
|
|
* 从路由配置中获取组件的 import 函数
|
2026-01-16 21:43:39 +08:00
|
|
|
|
*/
|
2026-01-16 22:07:39 +08:00
|
|
|
|
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
|
2026-01-16 21:43:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-16 22:07:39 +08:00
|
|
|
|
* 获取当前路由应该预加载的路由路径列表
|
2026-01-16 21:43:39 +08:00
|
|
|
|
*/
|
2026-01-16 22:07:39 +08:00
|
|
|
|
const getPrefetchPaths = (route: RouteLocationNormalized): string[] => {
|
|
|
|
|
|
return PREFETCH_ADJACENCY[route.path] || []
|
2026-01-16 21:43:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 执行单个组件的预加载
|
|
|
|
|
|
*/
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
2026-01-16 22:07:39 +08:00
|
|
|
|
const prefetchPaths = getPrefetchPaths(route)
|
|
|
|
|
|
if (prefetchPaths.length === 0) return
|
2026-01-16 21:43:39 +08:00
|
|
|
|
|
|
|
|
|
|
pendingPrefetchHandle.value = scheduleIdleCallback(
|
|
|
|
|
|
() => {
|
|
|
|
|
|
pendingPrefetchHandle.value = null
|
|
|
|
|
|
|
|
|
|
|
|
const routePath = route.path
|
2026-01-16 22:07:39 +08:00
|
|
|
|
if (prefetchedRoutes.value.has(routePath)) return
|
|
|
|
|
|
|
|
|
|
|
|
// 获取需要预加载的组件 import 函数
|
|
|
|
|
|
const importFns: ComponentImportFn[] = []
|
|
|
|
|
|
for (const path of prefetchPaths) {
|
|
|
|
|
|
const importFn = getComponentImporter(path)
|
|
|
|
|
|
if (importFn) {
|
|
|
|
|
|
importFns.push(importFn)
|
|
|
|
|
|
}
|
2026-01-16 21:43:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 22:07:39 +08:00
|
|
|
|
if (importFns.length > 0) {
|
|
|
|
|
|
Promise.all(importFns.map(prefetchComponent)).then(() => {
|
|
|
|
|
|
prefetchedRoutes.value.add(routePath)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-01-16 21:43:39 +08:00
|
|
|
|
},
|
2026-01-16 22:07:39 +08:00
|
|
|
|
{ timeout: 2000 }
|
2026-01-16 21:43:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-16 22:07:39 +08:00
|
|
|
|
* 重置预加载状态
|
2026-01-16 21:43:39 +08:00
|
|
|
|
*/
|
|
|
|
|
|
const resetPrefetchState = (): void => {
|
|
|
|
|
|
cancelPendingPrefetch()
|
|
|
|
|
|
prefetchedRoutes.value.clear()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 22:07:39 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 判断是否为管理员路由
|
|
|
|
|
|
*/
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 21:43:39 +08:00
|
|
|
|
return {
|
|
|
|
|
|
prefetchedRoutes: readonly(prefetchedRoutes),
|
|
|
|
|
|
triggerPrefetch,
|
|
|
|
|
|
cancelPendingPrefetch,
|
|
|
|
|
|
resetPrefetchState,
|
|
|
|
|
|
_getPrefetchConfig: getPrefetchConfig,
|
|
|
|
|
|
_isAdminRoute: isAdminRoute
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 22:07:39 +08:00
|
|
|
|
// 兼容旧测试的导出
|
|
|
|
|
|
export const _adminPrefetchMap = PREFETCH_ADJACENCY
|
|
|
|
|
|
export const _userPrefetchMap = PREFETCH_ADJACENCY
|