Search K
Appearance
Appearance
📊 SEO元描述:2024年最新Vue路由懒加载教程,详解动态导入、代码分割、预加载策略。包含完整优化方案,适合Vue.js开发者快速掌握路由性能优化技术。
核心关键词:Vue路由懒加载2024、Vue Router代码分割、动态导入、路由预加载、Vue性能优化
长尾关键词:Vue路由懒加载怎么实现、Vue Router代码分割最佳实践、动态导入如何使用、路由预加载策略、前端代码分割优化
通过本节Vue路由懒加载深度教程,你将系统性掌握:
为什么路由懒加载如此重要?这是现代单页应用性能优化的核心问题。随着应用规模增长,打包后的JavaScript文件会变得越来越大,路由懒加载通过代码分割技术,将不同路由的代码分别打包,实现按需加载,也是大型Vue应用的必备技术。
💡 策略建议:根据路由访问频率、页面复杂度和用户行为模式制定差异化的懒加载策略
Vue Router支持多种路由懒加载的实现方式:
// router/index.js - 路由懒加载配置
import { createRouter, createWebHistory } from 'vue-router'
// 基础懒加载实现
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: {
requiresAuth: true,
preload: true // 标记需要预加载
}
},
{
path: '/analytics',
name: 'Analytics',
component: () => import(
/* webpackChunkName: "analytics" */
'@/views/Analytics.vue'
),
meta: {
requiresAuth: true,
heavy: true // 标记为重型页面
}
},
{
path: '/settings',
name: 'Settings',
component: () => import(
/* webpackChunkName: "settings" */
/* webpackPreload: true */
'@/views/Settings.vue'
)
},
{
path: '/admin',
name: 'Admin',
component: () => import(
/* webpackChunkName: "admin" */
/* webpackPrefetch: true */
'@/views/admin/AdminLayout.vue'
),
children: [
{
path: 'users',
name: 'AdminUsers',
component: () => import('@/views/admin/Users.vue')
},
{
path: 'reports',
name: 'AdminReports',
component: () => import('@/views/admin/Reports.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router<!-- views/LazyLoadingDemo.vue - 路由懒加载演示组件 -->
<template>
<div class="lazy-loading-demo">
<div class="demo-section">
<h3>路由懒加载演示</h3>
<!-- 路由导航 -->
<div class="route-navigation">
<h4>路由导航</h4>
<div class="nav-buttons">
<router-link
v-for="route in demoRoutes"
:key="route.name"
:to="route.path"
class="nav-button"
:class="{
active: $route.path === route.path,
loading: loadingRoutes.includes(route.name),
loaded: loadedRoutes.includes(route.name)
}"
@click="onRouteClick(route)"
>
<span class="route-name">{{ route.label }}</span>
<span class="route-status">
<span v-if="loadingRoutes.includes(route.name)" class="loading-indicator">⏳</span>
<span v-else-if="loadedRoutes.includes(route.name)" class="loaded-indicator">✅</span>
<span v-else class="unloaded-indicator">📦</span>
</span>
</router-link>
</div>
</div>
<!-- 加载状态监控 -->
<div class="loading-monitor">
<h4>加载状态监控</h4>
<div class="monitor-stats">
<div class="stat-item">
<span class="stat-label">已加载路由:</span>
<span class="stat-value">{{ loadedRoutes.length }}</span>
</div>
<div class="stat-item">
<span class="stat-label">总路由数:</span>
<span class="stat-value">{{ demoRoutes.length }}</span>
</div>
<div class="stat-item">
<span class="stat-label">加载进度:</span>
<span class="stat-value">{{ loadingProgress }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">平均加载时间:</span>
<span class="stat-value">{{ averageLoadTime }}ms</span>
</div>
</div>
<div class="loading-timeline">
<h5>加载时间线</h5>
<div class="timeline-container">
<div
v-for="record in loadingRecords"
:key="record.route"
class="timeline-item"
>
<div class="timeline-route">{{ record.route }}</div>
<div class="timeline-time">{{ record.loadTime }}ms</div>
<div class="timeline-bar">
<div
class="timeline-fill"
:style="{ width: getTimelineWidth(record.loadTime) + '%' }"
></div>
</div>
</div>
</div>
</div>
</div>
<!-- 预加载控制 -->
<div class="preload-control">
<h4>预加载控制</h4>
<div class="preload-options">
<label>
<input
type="checkbox"
v-model="enablePreload"
@change="togglePreload"
>
启用智能预加载
</label>
<label>
<input
type="checkbox"
v-model="enablePrefetch"
@change="togglePrefetch"
>
启用预取(Prefetch)
</label>
<label>
<input
type="checkbox"
v-model="enablePreloadOnHover"
@change="togglePreloadOnHover"
>
悬停时预加载
</label>
</div>
<div class="preload-actions">
<button @click="preloadAllRoutes">预加载所有路由</button>
<button @click="preloadCriticalRoutes">预加载关键路由</button>
<button @click="clearRouteCache">清空路由缓存</button>
</div>
<div class="preload-status">
<h5>预加载状态</h5>
<div class="preload-list">
<div
v-for="route in preloadedRoutes"
:key="route"
class="preload-item"
>
<span class="preload-route">{{ route }}</span>
<span class="preload-indicator">🚀</span>
</div>
</div>
</div>
</div>
<!-- 路由内容区域 -->
<div class="route-content">
<h4>路由内容</h4>
<div class="content-container">
<Suspense>
<template #default>
<router-view />
</template>
<template #fallback>
<div class="route-loading">
<div class="loading-spinner"></div>
<div class="loading-text">正在加载页面...</div>
<div class="loading-progress">
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: routeLoadingProgress + '%' }"
></div>
</div>
<div class="progress-text">{{ routeLoadingProgress }}%</div>
</div>
</div>
</template>
</Suspense>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'LazyLoadingDemo',
data() {
return {
demoRoutes: [
{ name: 'Home', path: '/', label: '首页' },
{ name: 'About', path: '/about', label: '关于' },
{ name: 'Dashboard', path: '/dashboard', label: '仪表板' },
{ name: 'Analytics', path: '/analytics', label: '分析' },
{ name: 'Settings', path: '/settings', label: '设置' },
{ name: 'Admin', path: '/admin', label: '管理' }
],
loadingRoutes: [],
loadedRoutes: [],
loadingRecords: [],
preloadedRoutes: [],
enablePreload: true,
enablePrefetch: false,
enablePreloadOnHover: true,
routeLoadingProgress: 0,
routeLoadStartTime: 0
}
},
computed: {
loadingProgress() {
return Math.round((this.loadedRoutes.length / this.demoRoutes.length) * 100)
},
averageLoadTime() {
if (this.loadingRecords.length === 0) return 0
const total = this.loadingRecords.reduce((sum, record) => sum + record.loadTime, 0)
return Math.round(total / this.loadingRecords.length)
}
},
mounted() {
this.initRouteMonitoring()
this.setupPreloadStrategies()
},
methods: {
initRouteMonitoring() {
// 监控路由变化
this.$router.beforeEach((to, from, next) => {
this.routeLoadStartTime = performance.now()
this.routeLoadingProgress = 0
if (!this.loadingRoutes.includes(to.name)) {
this.loadingRoutes.push(to.name)
}
// 模拟加载进度
this.simulateLoadingProgress()
next()
})
this.$router.afterEach((to, from) => {
const loadTime = performance.now() - this.routeLoadStartTime
// 记录加载完成
this.loadingRoutes = this.loadingRoutes.filter(route => route !== to.name)
if (!this.loadedRoutes.includes(to.name)) {
this.loadedRoutes.push(to.name)
this.loadingRecords.push({
route: to.name,
loadTime: Math.round(loadTime),
timestamp: Date.now()
})
}
this.routeLoadingProgress = 100
})
},
simulateLoadingProgress() {
// 模拟路由加载进度
const interval = setInterval(() => {
this.routeLoadingProgress += Math.random() * 20
if (this.routeLoadingProgress >= 90) {
this.routeLoadingProgress = 90
clearInterval(interval)
}
}, 100)
},
setupPreloadStrategies() {
// 设置预加载策略
if (this.enablePreloadOnHover) {
this.setupHoverPreload()
}
// 智能预加载关键路由
if (this.enablePreload) {
this.preloadCriticalRoutes()
}
},
setupHoverPreload() {
// 悬停预加载
this.$nextTick(() => {
const navButtons = this.$el.querySelectorAll('.nav-button')
navButtons.forEach(button => {
button.addEventListener('mouseenter', (event) => {
if (this.enablePreloadOnHover) {
const routePath = button.getAttribute('to')
this.preloadRoute(routePath)
}
})
})
})
},
onRouteClick(route) {
console.log(`Navigating to ${route.name}`)
},
preloadRoute(routePath) {
// 预加载指定路由
const route = this.demoRoutes.find(r => r.path === routePath)
if (route && !this.preloadedRoutes.includes(route.name)) {
console.log(`Preloading route: ${route.name}`)
// 模拟预加载
setTimeout(() => {
this.preloadedRoutes.push(route.name)
}, 500)
}
},
preloadAllRoutes() {
// 预加载所有路由
this.demoRoutes.forEach(route => {
if (!this.preloadedRoutes.includes(route.name)) {
this.preloadRoute(route.path)
}
})
},
preloadCriticalRoutes() {
// 预加载关键路由
const criticalRoutes = ['Dashboard', 'Settings']
criticalRoutes.forEach(routeName => {
const route = this.demoRoutes.find(r => r.name === routeName)
if (route) {
this.preloadRoute(route.path)
}
})
},
clearRouteCache() {
// 清空路由缓存(模拟)
this.loadedRoutes = []
this.preloadedRoutes = []
this.loadingRecords = []
console.log('Route cache cleared')
},
togglePreload() {
if (this.enablePreload) {
this.preloadCriticalRoutes()
}
},
togglePrefetch() {
// 切换预取功能
console.log(`Prefetch ${this.enablePrefetch ? 'enabled' : 'disabled'}`)
},
togglePreloadOnHover() {
if (this.enablePreloadOnHover) {
this.setupHoverPreload()
}
},
getTimelineWidth(loadTime) {
const maxTime = Math.max(...this.loadingRecords.map(r => r.loadTime))
return (loadTime / maxTime) * 100
}
}
}
</script>
<style scoped>
.demo-section {
padding: 24px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.route-navigation,
.loading-monitor,
.preload-control,
.route-content {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e9ecef;
border-radius: 8px;
}
.nav-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.nav-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: white;
border: 2px solid #007bff;
color: #007bff;
text-decoration: none;
border-radius: 6px;
transition: all 0.3s ease;
position: relative;
}
.nav-button:hover {
background: #f8f9fa;
transform: translateY(-2px);
}
.nav-button.active {
background: #007bff;
color: white;
}
.nav-button.loading {
border-color: #ffc107;
color: #ffc107;
}
.nav-button.loaded {
border-color: #28a745;
}
.route-name {
font-weight: 500;
}
.route-status {
font-size: 14px;
}
.monitor-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.stat-item {
padding: 12px 16px;
background: #f8f9fa;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.stat-label {
font-size: 14px;
color: #666;
display: block;
margin-bottom: 4px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #007bff;
}
.timeline-container {
max-height: 200px;
overflow-y: auto;
}
.timeline-item {
display: grid;
grid-template-columns: 100px 80px 1fr;
gap: 12px;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.timeline-route {
font-size: 14px;
font-weight: 500;
}
.timeline-time {
font-size: 12px;
color: #666;
font-family: monospace;
}
.timeline-bar {
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.timeline-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #0056b3);
transition: width 0.3s ease;
}
.preload-options {
display: flex;
gap: 20px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.preload-options label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.preload-actions {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.preload-actions button {
padding: 8px 16px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.preload-actions button:hover {
background: #1e7e34;
}
.preload-list {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.preload-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: #e3f2fd;
border-radius: 16px;
font-size: 14px;
}
.content-container {
min-height: 200px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 20px;
}
.route-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 160px;
text-align: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 16px;
color: #666;
margin-bottom: 16px;
}
.loading-progress {
width: 200px;
display: flex;
align-items: center;
gap: 12px;
}
.progress-bar {
flex: 1;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #0056b3);
transition: width 0.3s ease;
}
.progress-text {
font-size: 12px;
color: #666;
font-family: monospace;
min-width: 35px;
}
</style>代码分割是将应用代码拆分成多个bundle的技术,实现更精细的加载控制:
// webpack.config.js - 代码分割配置
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 第三方库分割
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
// Vue相关库分割
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/,
name: 'vue',
chunks: 'all',
priority: 20
},
// UI库分割
ui: {
test: /[\\/]node_modules[\\/](element-plus|ant-design-vue)[\\/]/,
name: 'ui',
chunks: 'all',
priority: 15
},
// 工具库分割
utils: {
test: /[\\/]node_modules[\\/](lodash|moment|axios)[\\/]/,
name: 'utils',
chunks: 'all',
priority: 12
},
// 公共代码分割
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
}
}
}代码分割核心策略:
💼 分割提示:合理的代码分割能显著提升缓存命中率和加载性能,但过度分割会增加HTTP请求数量,需要找到平衡点
智能预加载能够在用户需要之前提前加载资源,提升用户体验:
// utils/routePreloader.js - 路由预加载器
class RoutePreloader {
constructor(router, options = {}) {
this.router = router
this.options = {
preloadDelay: 2000, // 预加载延迟
hoverDelay: 100, // 悬停预加载延迟
maxConcurrent: 3, // 最大并发预加载数
enableAnalytics: true, // 启用分析
...options
}
this.preloadQueue = []
this.preloadedRoutes = new Set()
this.loadingRoutes = new Set()
this.analytics = {
preloadHits: 0,
preloadMisses: 0,
totalPreloads: 0
}
this.init()
}
init() {
this.setupRouteAnalytics()
this.setupIntersectionObserver()
this.setupIdlePreloading()
}
setupRouteAnalytics() {
// 分析路由访问模式
this.router.afterEach((to, from) => {
if (this.preloadedRoutes.has(to.name)) {
this.analytics.preloadHits++
} else {
this.analytics.preloadMisses++
}
// 记录路由转换模式
this.recordRouteTransition(from.name, to.name)
})
}
setupIntersectionObserver() {
// 监控链接可见性
this.linkObserver = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const link = entry.target
const routeName = link.dataset.routeName
if (routeName) {
this.schedulePreload(routeName, 'viewport')
}
}
})
},
{ threshold: 0.1 }
)
}
setupIdlePreloading() {
// 空闲时预加载
if ('requestIdleCallback' in window) {
const idlePreload = () => {
requestIdleCallback(() => {
this.preloadCriticalRoutes()
setTimeout(idlePreload, this.options.preloadDelay)
})
}
idlePreload()
}
}
preloadRoute(routeName, priority = 'normal') {
if (this.preloadedRoutes.has(routeName) || this.loadingRoutes.has(routeName)) {
return Promise.resolve()
}
this.loadingRoutes.add(routeName)
this.analytics.totalPreloads++
return new Promise((resolve, reject) => {
const route = this.router.getRoutes().find(r => r.name === routeName)
if (!route) {
reject(new Error(`Route ${routeName} not found`))
return
}
// 动态导入路由组件
const componentLoader = route.component
if (typeof componentLoader === 'function') {
componentLoader()
.then(() => {
this.preloadedRoutes.add(routeName)
this.loadingRoutes.delete(routeName)
this.onPreloadComplete(routeName, priority)
resolve()
})
.catch(error => {
this.loadingRoutes.delete(routeName)
this.onPreloadError(routeName, error)
reject(error)
})
} else {
// 组件已经加载
this.preloadedRoutes.add(routeName)
this.loadingRoutes.delete(routeName)
resolve()
}
})
}
schedulePreload(routeName, trigger) {
const priority = this.getPreloadPriority(routeName, trigger)
if (this.loadingRoutes.size >= this.options.maxConcurrent) {
this.preloadQueue.push({ routeName, priority, trigger })
return
}
this.preloadRoute(routeName, priority)
.then(() => this.processPreloadQueue())
.catch(console.error)
}
getPreloadPriority(routeName, trigger) {
const route = this.router.getRoutes().find(r => r.name === routeName)
const meta = route?.meta || {}
// 基于触发方式和路由元信息确定优先级
if (trigger === 'hover') return 'high'
if (trigger === 'viewport') return 'medium'
if (meta.critical) return 'high'
if (meta.preload) return 'medium'
return 'low'
}
processPreloadQueue() {
if (this.preloadQueue.length === 0) return
// 按优先级排序
this.preloadQueue.sort((a, b) => {
const priorities = { high: 3, medium: 2, low: 1 }
return priorities[b.priority] - priorities[a.priority]
})
while (this.preloadQueue.length > 0 && this.loadingRoutes.size < this.options.maxConcurrent) {
const { routeName, priority } = this.preloadQueue.shift()
this.preloadRoute(routeName, priority)
}
}
preloadCriticalRoutes() {
// 预加载关键路由
const criticalRoutes = this.router.getRoutes()
.filter(route => route.meta?.critical)
.map(route => route.name)
criticalRoutes.forEach(routeName => {
this.schedulePreload(routeName, 'critical')
})
}
preloadByUserBehavior() {
// 基于用户行为预测预加载
const predictions = this.predictNextRoutes()
predictions.forEach(({ routeName, probability }) => {
if (probability > 0.7) {
this.schedulePreload(routeName, 'prediction')
}
})
}
predictNextRoutes() {
// 简化的路由预测算法
const currentRoute = this.router.currentRoute.value.name
const transitions = this.getRouteTransitions(currentRoute)
return Object.entries(transitions)
.map(([routeName, count]) => ({
routeName,
probability: count / this.getTotalTransitions(currentRoute)
}))
.sort((a, b) => b.probability - a.probability)
.slice(0, 3)
}
recordRouteTransition(from, to) {
if (!from || !to) return
const key = `transitions_${from}`
const transitions = JSON.parse(localStorage.getItem(key) || '{}')
transitions[to] = (transitions[to] || 0) + 1
localStorage.setItem(key, JSON.stringify(transitions))
}
getRouteTransitions(routeName) {
const key = `transitions_${routeName}`
return JSON.parse(localStorage.getItem(key) || '{}')
}
getTotalTransitions(routeName) {
const transitions = this.getRouteTransitions(routeName)
return Object.values(transitions).reduce((sum, count) => sum + count, 0)
}
onPreloadComplete(routeName, priority) {
console.log(`Preloaded route: ${routeName} (${priority} priority)`)
if (this.options.enableAnalytics) {
this.sendAnalytics('preload_complete', {
route: routeName,
priority,
timestamp: Date.now()
})
}
}
onPreloadError(routeName, error) {
console.error(`Failed to preload route: ${routeName}`, error)
if (this.options.enableAnalytics) {
this.sendAnalytics('preload_error', {
route: routeName,
error: error.message,
timestamp: Date.now()
})
}
}
sendAnalytics(event, data) {
// 发送分析数据到服务器
if (typeof gtag !== 'undefined') {
gtag('event', event, {
custom_parameter: data
})
}
}
getAnalytics() {
return {
...this.analytics,
hitRate: this.analytics.preloadHits / (this.analytics.preloadHits + this.analytics.preloadMisses),
preloadedCount: this.preloadedRoutes.size
}
}
clearCache() {
this.preloadedRoutes.clear()
this.preloadQueue = []
this.analytics = {
preloadHits: 0,
preloadMisses: 0,
totalPreloads: 0
}
}
}
export default RoutePreloader// utils/routeCache.js - 路由缓存管理
class RouteCacheManager {
constructor(options = {}) {
this.options = {
maxCacheSize: 50, // 最大缓存数量
ttl: 30 * 60 * 1000, // 缓存TTL (30分钟)
enablePersistence: true, // 启用持久化
compressionEnabled: true, // 启用压缩
...options
}
this.cache = new Map()
this.accessTimes = new Map()
this.cacheSizes = new Map()
this.init()
}
init() {
if (this.options.enablePersistence) {
this.loadFromStorage()
}
// 定期清理过期缓存
setInterval(() => {
this.cleanExpiredCache()
}, 5 * 60 * 1000) // 每5分钟清理一次
}
set(key, value, options = {}) {
const cacheEntry = {
value,
timestamp: Date.now(),
ttl: options.ttl || this.options.ttl,
size: this.calculateSize(value),
compressed: false
}
// 压缩大型缓存项
if (this.options.compressionEnabled && cacheEntry.size > 10240) {
cacheEntry.value = this.compress(value)
cacheEntry.compressed = true
}
// 检查缓存大小限制
if (this.cache.size >= this.options.maxCacheSize) {
this.evictLRU()
}
this.cache.set(key, cacheEntry)
this.accessTimes.set(key, Date.now())
this.cacheSizes.set(key, cacheEntry.size)
if (this.options.enablePersistence) {
this.saveToStorage()
}
}
get(key) {
const entry = this.cache.get(key)
if (!entry) return null
// 检查TTL
if (Date.now() - entry.timestamp > entry.ttl) {
this.delete(key)
return null
}
// 更新访问时间
this.accessTimes.set(key, Date.now())
// 解压缩
if (entry.compressed) {
return this.decompress(entry.value)
}
return entry.value
}
delete(key) {
this.cache.delete(key)
this.accessTimes.delete(key)
this.cacheSizes.delete(key)
if (this.options.enablePersistence) {
this.saveToStorage()
}
}
evictLRU() {
// 移除最近最少使用的缓存项
let oldestKey = null
let oldestTime = Infinity
for (const [key, time] of this.accessTimes) {
if (time < oldestTime) {
oldestTime = time
oldestKey = key
}
}
if (oldestKey) {
this.delete(oldestKey)
}
}
cleanExpiredCache() {
const now = Date.now()
const expiredKeys = []
for (const [key, entry] of this.cache) {
if (now - entry.timestamp > entry.ttl) {
expiredKeys.push(key)
}
}
expiredKeys.forEach(key => this.delete(key))
}
calculateSize(value) {
// 简化的大小计算
return JSON.stringify(value).length
}
compress(value) {
// 简化的压缩实现(实际项目中可使用LZ-string等库)
return JSON.stringify(value)
}
decompress(value) {
// 简化的解压缩实现
return JSON.parse(value)
}
loadFromStorage() {
try {
const stored = localStorage.getItem('routeCache')
if (stored) {
const data = JSON.parse(stored)
this.cache = new Map(data.cache)
this.accessTimes = new Map(data.accessTimes)
this.cacheSizes = new Map(data.cacheSizes)
}
} catch (error) {
console.error('Failed to load cache from storage:', error)
}
}
saveToStorage() {
try {
const data = {
cache: Array.from(this.cache.entries()),
accessTimes: Array.from(this.accessTimes.entries()),
cacheSizes: Array.from(this.cacheSizes.entries())
}
localStorage.setItem('routeCache', JSON.stringify(data))
} catch (error) {
console.error('Failed to save cache to storage:', error)
}
}
getStats() {
const totalSize = Array.from(this.cacheSizes.values())
.reduce((sum, size) => sum + size, 0)
return {
size: this.cache.size,
maxSize: this.options.maxCacheSize,
totalSize,
averageSize: totalSize / this.cache.size || 0,
hitRate: this.calculateHitRate()
}
}
calculateHitRate() {
// 简化的命中率计算
return 0.85 // 实际项目中需要跟踪命中和未命中次数
}
clear() {
this.cache.clear()
this.accessTimes.clear()
this.cacheSizes.clear()
if (this.options.enablePersistence) {
localStorage.removeItem('routeCache')
}
}
}
export default RouteCacheManager<!-- components/RoutePerformanceMonitor.vue -->
<template>
<div class="route-performance-monitor">
<div class="monitor-header">
<h4>路由性能监控</h4>
<div class="monitor-controls">
<button @click="toggleMonitoring">
{{ isMonitoring ? '停止' : '开始' }}监控
</button>
<button @click="clearData">清空数据</button>
<button @click="exportData">导出数据</button>
</div>
</div>
<div class="performance-overview">
<div class="metric-card">
<div class="metric-title">平均加载时间</div>
<div class="metric-value">{{ averageLoadTime }}ms</div>
<div class="metric-trend" :class="loadTimeTrend">
{{ loadTimeTrend === 'up' ? '↗️' : loadTimeTrend === 'down' ? '↘️' : '➡️' }}
</div>
</div>
<div class="metric-card">
<div class="metric-title">缓存命中率</div>
<div class="metric-value">{{ cacheHitRate }}%</div>
<div class="metric-trend" :class="cacheHitTrend">
{{ cacheHitTrend === 'up' ? '↗️' : cacheHitTrend === 'down' ? '↘️' : '➡️' }}
</div>
</div>
<div class="metric-card">
<div class="metric-title">预加载成功率</div>
<div class="metric-value">{{ preloadSuccessRate }}%</div>
<div class="metric-trend" :class="preloadTrend">
{{ preloadTrend === 'up' ? '↗️' : preloadTrend === 'down' ? '↘️' : '➡️' }}
</div>
</div>
<div class="metric-card">
<div class="metric-title">总路由访问</div>
<div class="metric-value">{{ totalRouteVisits }}</div>
<div class="metric-trend up">📈</div>
</div>
</div>
<div class="performance-charts">
<div class="chart-container">
<h5>加载时间趋势</h5>
<div class="load-time-chart">
<div
v-for="(point, index) in loadTimeHistory"
:key="index"
class="chart-bar"
:style="{
height: getBarHeight(point.time) + '%',
backgroundColor: getBarColor(point.time)
}"
:title="`${point.route}: ${point.time}ms`"
></div>
</div>
</div>
<div class="route-stats">
<h5>路由统计</h5>
<div class="stats-table">
<div class="table-header">
<div class="col-route">路由</div>
<div class="col-visits">访问次数</div>
<div class="col-avg-time">平均时间</div>
<div class="col-cache-hit">缓存命中</div>
</div>
<div
v-for="stat in routeStats"
:key="stat.route"
class="table-row"
>
<div class="col-route">{{ stat.route }}</div>
<div class="col-visits">{{ stat.visits }}</div>
<div class="col-avg-time">{{ stat.avgTime }}ms</div>
<div class="col-cache-hit">{{ stat.cacheHitRate }}%</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'RoutePerformanceMonitor',
data() {
return {
isMonitoring: true,
loadTimeHistory: [],
routeStats: [],
performanceData: {
totalLoadTime: 0,
totalRoutes: 0,
cacheHits: 0,
cacheMisses: 0,
preloadSuccesses: 0,
preloadFailures: 0
},
previousMetrics: {
averageLoadTime: 0,
cacheHitRate: 0,
preloadSuccessRate: 0
}
}
},
computed: {
averageLoadTime() {
if (this.performanceData.totalRoutes === 0) return 0
return Math.round(this.performanceData.totalLoadTime / this.performanceData.totalRoutes)
},
cacheHitRate() {
const total = this.performanceData.cacheHits + this.performanceData.cacheMisses
if (total === 0) return 0
return Math.round((this.performanceData.cacheHits / total) * 100)
},
preloadSuccessRate() {
const total = this.performanceData.preloadSuccesses + this.performanceData.preloadFailures
if (total === 0) return 0
return Math.round((this.performanceData.preloadSuccesses / total) * 100)
},
totalRouteVisits() {
return this.performanceData.totalRoutes
},
loadTimeTrend() {
return this.getTrend(this.averageLoadTime, this.previousMetrics.averageLoadTime)
},
cacheHitTrend() {
return this.getTrend(this.cacheHitRate, this.previousMetrics.cacheHitRate)
},
preloadTrend() {
return this.getTrend(this.preloadSuccessRate, this.previousMetrics.preloadSuccessRate)
}
},
mounted() {
this.initMonitoring()
},
methods: {
initMonitoring() {
if (!this.isMonitoring) return
// 监控路由变化
this.$router.beforeEach((to, from, next) => {
this.routeStartTime = performance.now()
next()
})
this.$router.afterEach((to, from) => {
if (this.isMonitoring) {
const loadTime = performance.now() - this.routeStartTime
this.recordRoutePerformance(to.name, loadTime)
}
})
},
recordRoutePerformance(routeName, loadTime) {
// 记录性能数据
this.performanceData.totalLoadTime += loadTime
this.performanceData.totalRoutes++
// 添加到历史记录
this.loadTimeHistory.push({
route: routeName,
time: Math.round(loadTime),
timestamp: Date.now()
})
// 保持历史记录在合理范围内
if (this.loadTimeHistory.length > 50) {
this.loadTimeHistory.shift()
}
// 更新路由统计
this.updateRouteStats(routeName, loadTime)
},
updateRouteStats(routeName, loadTime) {
let stat = this.routeStats.find(s => s.route === routeName)
if (!stat) {
stat = {
route: routeName,
visits: 0,
totalTime: 0,
avgTime: 0,
cacheHits: 0,
cacheMisses: 0,
cacheHitRate: 0
}
this.routeStats.push(stat)
}
stat.visits++
stat.totalTime += loadTime
stat.avgTime = Math.round(stat.totalTime / stat.visits)
// 模拟缓存命中数据
if (Math.random() > 0.3) {
stat.cacheHits++
this.performanceData.cacheHits++
} else {
stat.cacheMisses++
this.performanceData.cacheMisses++
}
stat.cacheHitRate = Math.round((stat.cacheHits / (stat.cacheHits + stat.cacheMisses)) * 100)
},
getTrend(current, previous) {
if (previous === 0) return 'stable'
const change = ((current - previous) / previous) * 100
if (change > 5) return 'up'
if (change < -5) return 'down'
return 'stable'
},
getBarHeight(time) {
const maxTime = Math.max(...this.loadTimeHistory.map(p => p.time))
return (time / maxTime) * 100
},
getBarColor(time) {
if (time < 100) return '#28a745'
if (time < 300) return '#ffc107'
return '#dc3545'
},
toggleMonitoring() {
this.isMonitoring = !this.isMonitoring
if (this.isMonitoring) {
this.initMonitoring()
}
},
clearData() {
this.loadTimeHistory = []
this.routeStats = []
this.performanceData = {
totalLoadTime: 0,
totalRoutes: 0,
cacheHits: 0,
cacheMisses: 0,
preloadSuccesses: 0,
preloadFailures: 0
}
},
exportData() {
const data = {
loadTimeHistory: this.loadTimeHistory,
routeStats: this.routeStats,
performanceData: this.performanceData,
exportTime: new Date().toISOString()
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `route-performance-${Date.now()}.json`
a.click()
URL.revokeObjectURL(url)
}
}
}
</script>
<style scoped>
.route-performance-monitor {
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.monitor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.monitor-controls {
display: flex;
gap: 8px;
}
.monitor-controls button {
padding: 6px 12px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.performance-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 30px;
}
.metric-card {
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
text-align: center;
position: relative;
}
.metric-title {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.metric-value {
font-size: 24px;
font-weight: bold;
color: #007bff;
margin-bottom: 4px;
}
.metric-trend {
position: absolute;
top: 8px;
right: 8px;
font-size: 16px;
}
.metric-trend.up {
color: #28a745;
}
.metric-trend.down {
color: #dc3545;
}
.metric-trend.stable {
color: #6c757d;
}
.performance-charts {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.chart-container {
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
}
.load-time-chart {
display: flex;
align-items: end;
gap: 2px;
height: 100px;
margin-top: 12px;
}
.chart-bar {
flex: 1;
min-height: 2px;
border-radius: 2px 2px 0 0;
cursor: pointer;
}
.route-stats {
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
}
.stats-table {
margin-top: 12px;
}
.table-header,
.table-row {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 12px;
padding: 8px 0;
align-items: center;
}
.table-header {
font-weight: bold;
border-bottom: 2px solid #ddd;
font-size: 14px;
}
.table-row {
border-bottom: 1px solid #eee;
font-size: 13px;
}
.table-row:hover {
background: #f8f9fa;
}
</style>通过本节Vue路由懒加载深度教程的学习,你已经掌握:
A: 当应用有多个页面、打包文件较大(>1MB)、首屏加载时间过长时使用。小型应用可能不需要懒加载。
A: 基于用户行为数据制定预加载策略,优先预加载高频访问的路由,避免预加载所有路由造成带宽浪费。
A: 是的,但现代浏览器支持HTTP/2多路复用,多个小文件的加载效率通常比单个大文件更好,且有利于缓存。
A: 使用Suspense组件的error boundary,提供重试机制,记录加载失败的路由,必要时回退到同步加载。
A: 对客户端渲染的SPA有影响,建议结合SSR或预渲染技术。对于管理后台等不需要SEO的应用影响较小。
// vue.config.js - 生产环境优化配置
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
}
}
},
chainWebpack: config => {
// 预加载关键路由
config.plugin('preload').tap(options => {
options[0] = {
rel: 'preload',
include: 'initial',
fileBlacklist: [/\.map$/, /hot-update\.js$/]
}
return options
})
// 预取非关键路由
config.plugin('prefetch').tap(options => {
options[0].fileBlacklist = options[0].fileBlacklist || []
options[0].fileBlacklist.push(/runtime\..*\.js$/)
return options
})
}
}"路由懒加载是现代Web应用性能优化的基石技术。通过合理的代码分割、智能的预加载策略和有效的缓存管理,我们能够显著提升应用的加载速度和用户体验。记住,优化应该基于真实的用户数据和性能指标,持续监控和改进是成功的关键!"