File size: 5,726 Bytes
8059bf0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | /**
* 路由预加载组合式函数
* 在浏览器空闲时预加载可能访问的下一个页面,提升导航体验
*
* 优化说明:
* - 不使用静态 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
|