perf(路由预加载): 修复静态 import 导致入口文件膨胀问题

问题:
- 原实现使用静态 import() 映射表
- Rollup 静态分析时将所有 37 个视图组件引用打包进 index.js
- 导致首次加载时需要解析大量未使用的 import 语句

修复:
- 移除静态 import() 映射,改用纯路径字符串邻接表
- 通过 router.getRoutes() 动态获取组件的 import 函数
- 延迟初始化 routePrefetch,首次导航时才创建实例
- 更新测试文件使用 mock router

效果:
- index.js 中动态 import 引用从 37 个减少到 1 个
- 首次加载不再包含未使用的视图组件引用
- 41 个测试全部通过

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
yangjianbo
2026-01-16 22:07:39 +08:00
parent d9433699db
commit b0569d873a
3 changed files with 143 additions and 215 deletions

View File

@@ -2,20 +2,7 @@
* useRoutePrefetch 组合式函数单元测试
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import type { RouteLocationNormalized } from 'vue-router'
// Mock 所有动态 import
vi.mock('@/views/admin/AccountsView.vue', () => ({ default: {} }))
vi.mock('@/views/admin/UsersView.vue', () => ({ default: {} }))
vi.mock('@/views/admin/DashboardView.vue', () => ({ default: {} }))
vi.mock('@/views/admin/GroupsView.vue', () => ({ default: {} }))
vi.mock('@/views/admin/SubscriptionsView.vue', () => ({ default: {} }))
vi.mock('@/views/admin/RedeemView.vue', () => ({ default: {} }))
vi.mock('@/views/user/KeysView.vue', () => ({ default: {} }))
vi.mock('@/views/user/UsageView.vue', () => ({ default: {} }))
vi.mock('@/views/user/DashboardView.vue', () => ({ default: {} }))
vi.mock('@/views/user/RedeemView.vue', () => ({ default: {} }))
vi.mock('@/views/user/ProfileView.vue', () => ({ default: {} }))
import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue-router'
import { useRoutePrefetch, _adminPrefetchMap, _userPrefetchMap } from '../useRoutePrefetch'
@@ -32,11 +19,37 @@ const createMockRoute = (path: string): RouteLocationNormalized => ({
redirectedFrom: undefined
})
// Mock Router
const createMockRouter = (): Router => {
const mockImportFn = vi.fn().mockResolvedValue({ default: {} })
const routes: Partial<RouteRecordNormalized>[] = [
{ path: '/admin/dashboard', components: { default: mockImportFn } },
{ path: '/admin/accounts', components: { default: mockImportFn } },
{ path: '/admin/users', components: { default: mockImportFn } },
{ path: '/admin/groups', components: { default: mockImportFn } },
{ path: '/admin/subscriptions', components: { default: mockImportFn } },
{ path: '/admin/redeem', components: { default: mockImportFn } },
{ path: '/dashboard', components: { default: mockImportFn } },
{ path: '/keys', components: { default: mockImportFn } },
{ path: '/usage', components: { default: mockImportFn } },
{ path: '/redeem', components: { default: mockImportFn } },
{ path: '/profile', components: { default: mockImportFn } }
]
return {
getRoutes: () => routes as RouteRecordNormalized[]
} as Router
}
describe('useRoutePrefetch', () => {
let originalRequestIdleCallback: typeof window.requestIdleCallback
let originalCancelIdleCallback: typeof window.cancelIdleCallback
let mockRouter: Router
beforeEach(() => {
mockRouter = createMockRouter()
// 保存原始函数
originalRequestIdleCallback = window.requestIdleCallback
originalCancelIdleCallback = window.cancelIdleCallback
@@ -58,14 +71,14 @@ describe('useRoutePrefetch', () => {
describe('_isAdminRoute', () => {
it('应该正确识别管理员路由', () => {
const { _isAdminRoute } = useRoutePrefetch()
const { _isAdminRoute } = useRoutePrefetch(mockRouter)
expect(_isAdminRoute('/admin/dashboard')).toBe(true)
expect(_isAdminRoute('/admin/users')).toBe(true)
expect(_isAdminRoute('/admin/accounts')).toBe(true)
})
it('应该正确识别非管理员路由', () => {
const { _isAdminRoute } = useRoutePrefetch()
const { _isAdminRoute } = useRoutePrefetch(mockRouter)
expect(_isAdminRoute('/dashboard')).toBe(false)
expect(_isAdminRoute('/keys')).toBe(false)
expect(_isAdminRoute('/usage')).toBe(false)
@@ -74,7 +87,7 @@ describe('useRoutePrefetch', () => {
describe('_getPrefetchConfig', () => {
it('管理员 dashboard 应该返回正确的预加载配置', () => {
const { _getPrefetchConfig } = useRoutePrefetch()
const { _getPrefetchConfig } = useRoutePrefetch(mockRouter)
const route = createMockRoute('/admin/dashboard')
const config = _getPrefetchConfig(route)
@@ -82,7 +95,7 @@ describe('useRoutePrefetch', () => {
})
it('普通用户 dashboard 应该返回正确的预加载配置', () => {
const { _getPrefetchConfig } = useRoutePrefetch()
const { _getPrefetchConfig } = useRoutePrefetch(mockRouter)
const route = createMockRoute('/dashboard')
const config = _getPrefetchConfig(route)
@@ -90,7 +103,7 @@ describe('useRoutePrefetch', () => {
})
it('未定义的路由应该返回空数组', () => {
const { _getPrefetchConfig } = useRoutePrefetch()
const { _getPrefetchConfig } = useRoutePrefetch(mockRouter)
const route = createMockRoute('/unknown-route')
const config = _getPrefetchConfig(route)
@@ -100,7 +113,7 @@ describe('useRoutePrefetch', () => {
describe('triggerPrefetch', () => {
it('应该在浏览器空闲时触发预加载', async () => {
const { triggerPrefetch, prefetchedRoutes } = useRoutePrefetch()
const { triggerPrefetch, prefetchedRoutes } = useRoutePrefetch(mockRouter)
const route = createMockRoute('/admin/dashboard')
triggerPrefetch(route)
@@ -112,7 +125,7 @@ describe('useRoutePrefetch', () => {
})
it('应该避免重复预加载同一路由', async () => {
const { triggerPrefetch, prefetchedRoutes } = useRoutePrefetch()
const { triggerPrefetch, prefetchedRoutes } = useRoutePrefetch(mockRouter)
const route = createMockRoute('/admin/dashboard')
triggerPrefetch(route)
@@ -129,7 +142,7 @@ describe('useRoutePrefetch', () => {
describe('cancelPendingPrefetch', () => {
it('应该取消挂起的预加载任务', () => {
const { triggerPrefetch, cancelPendingPrefetch, prefetchedRoutes } = useRoutePrefetch()
const { triggerPrefetch, cancelPendingPrefetch, prefetchedRoutes } = useRoutePrefetch(mockRouter)
const route = createMockRoute('/admin/dashboard')
triggerPrefetch(route)
@@ -142,7 +155,7 @@ describe('useRoutePrefetch', () => {
describe('路由变化时取消之前的预加载', () => {
it('应该在路由变化时取消之前的预加载任务', async () => {
const { triggerPrefetch, prefetchedRoutes } = useRoutePrefetch()
const { triggerPrefetch, prefetchedRoutes } = useRoutePrefetch(mockRouter)
// 触发第一个路由的预加载
triggerPrefetch(createMockRoute('/admin/dashboard'))
@@ -160,7 +173,7 @@ describe('useRoutePrefetch', () => {
describe('resetPrefetchState', () => {
it('应该重置所有预加载状态', async () => {
const { triggerPrefetch, resetPrefetchState, prefetchedRoutes } = useRoutePrefetch()
const { triggerPrefetch, resetPrefetchState, prefetchedRoutes } = useRoutePrefetch(mockRouter)
const route = createMockRoute('/admin/dashboard')
triggerPrefetch(route)
@@ -194,7 +207,7 @@ describe('useRoutePrefetch', () => {
return setTimeout(() => cb({ didTimeout: true, timeRemaining: () => 0 }), timeout)
})
const { triggerPrefetch, prefetchedRoutes } = useRoutePrefetch()
const { triggerPrefetch, prefetchedRoutes } = useRoutePrefetch(mockRouter)
const route = createMockRoute('/dashboard')
triggerPrefetch(route)
@@ -208,12 +221,24 @@ describe('useRoutePrefetch', () => {
describe('预加载失败处理', () => {
it('预加载失败时应该静默处理不影响页面功能', async () => {
// 这个测试验证预加载失败不会抛出异常
const { triggerPrefetch } = useRoutePrefetch()
const { triggerPrefetch } = useRoutePrefetch(mockRouter)
const route = createMockRoute('/admin/dashboard')
// 不应该抛出异常
expect(() => triggerPrefetch(route)).not.toThrow()
})
})
describe('无 router 时的行为', () => {
it('没有传入 router 时应该正常工作但不执行预加载', async () => {
const { triggerPrefetch, prefetchedRoutes } = useRoutePrefetch()
const route = createMockRoute('/admin/dashboard')
triggerPrefetch(route)
await new Promise((resolve) => setTimeout(resolve, 100))
// 没有 router无法获取组件所以不会预加载
expect(prefetchedRoutes.value.size).toBe(0)
})
})
})