在 Vue 项目中,路由不仅仅是页面跳转的工具,更是整个应用架构的核心。一个优秀的路由配置能显著提升用户体验、代码可维护性和安全性。本文将详细讲解如何从零开始配置一个企业级的 Vue Router 系统。
一、路由配置的核心概念
1.1 为什么要关注路由配置?
在企业级项目中,路由承担着以下关键职责:
- 权限控制:控制用户能访问哪些页面
- 用户体验:管理页面切换动画和加载状态
- 性能优化:实现按需加载和组件缓存
- 错误处理:优雅处理404、500等错误
- SEO优化:管理页面标题和元信息
1.2 企业级路由 vs 普通路由
| 特性 | 普通路由配置 | 企业级路由配置 |
|---|---|---|
| 权限控制 | 简单的token检查 | 角色/权限分级控制 |
| 代码组织 | 所有配置在一个文件 | 模块化分离 |
| 错误处理 | 基本404页面 | 完整的错误边界 |
| 用户体验 | 无进度提示 | 进度条显示 |
| 类型安全 | 使用JavaScript | 完整的TypeScript类型检查支持 |
二、项目结构设计
2.1 推荐的目录结构
src/
├── router/
│ ├── index.ts # 路由实例和守卫
│ ├── routes/ # 路由配置目录
│ │ ├── index.ts # 路由配置入口
│ │ ├── modules/ # 模块化路由
│ │ │ ├── dashboard.ts
│ │ │ ├── article.ts
│ │ │ └── user.ts
│ │ └── static.ts # 静态路由(登录、错误页等)
│ └── guards/ # 路由守卫
│ ├── auth.ts # 认证守卫
│ ├── permission.ts # 权限守卫
│ └── progress.ts # 进度条守卫
├── views/ # 页面组件
├── layouts/ # 布局组件
└── utils/
└── nprogress.ts # 进度条工具
2.2 为什么要这样设计?
模块化分离的好处:
- 职责清晰:每个文件只做一件事
- 易于维护:修改一个功能不影响其他部分
- 便于测试:可以单独测试每个守卫
- 团队协作:不同开发者可以并行工作
三、TypeScript 类型增强
3.1 扩展路由元信息
首先创建类型定义文件:
typescript
// src/router/types/index.ts
import 'vue-router'
/**
* 扩展 Vue Router 的 RouteMeta 接口
* 这是企业级项目的关键一步,让 TypeScript 能识别我们的自定义元数据
*/
declare module 'vue-router' {
interface RouteMeta {
// 页面标题(用于浏览器标签页)
// 若想在页面title处显示
// 还需要在router.beforeEach处配置document.title = to.meta.title as string
title?: string
// 是否需要认证(登录)
requiresAuth?: boolean
// 是否缓存页面(配合 keep-alive)
keepAlive?: boolean
// 菜单图标(用于侧边栏)
icon?: string
// 是否在菜单中隐藏
hidden?: boolean
// 是否全屏页面(不显示布局)
fullScreen?: boolean
// 是否显示面包屑
breadcrumb?: boolean
// 页面权限标识(如:'user:view', 'article:edit')
permissions?: string[]
// 角色权限控制
roles?: string[]
// 页面加载提示文本
loadingText?: string
// 页面过渡动画
transition?: string
// 是否固定在标签页(不可关闭)
affix?: boolean
}
}
// 路由配置类型
export interface AppRouteRecordRaw {
path: string
name?: string
component?: Component
children?: AppRouteRecordRaw[]
meta?: RouteMeta
redirect?: string
[key: string]: any
}
为什么需要类型扩展?
- 代码提示 :编写
meta时会有自动补全 - 类型安全:防止拼写错误
- 文档化:清晰定义每个字段的作用
四、路由配置详解
4.1 模块化路由配置
typescript
// src/router/routes/modules/dashboard.ts
import type { RouteRecordRaw } from 'vue-router'
export const dashboardRoutes: RouteRecordRaw[] = [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: {
title: '仪表板',
icon: 'dashboard', // 侧边栏图标
keepAlive: true, // 需要缓存
affix: true, // 固定在标签页
requiresAuth: true, // 需要登录
permissions: ['dashboard:view'] // 需要的权限
}
},
{
path: '/dashboard/analytics',
name: 'DashboardAnalytics',
component: () => import('@/views/dashboard/analytics.vue'),
meta: {
title: '数据分析',
icon: 'chart',
keepAlive: true,
requiresAuth: true,
permissions: ['dashboard:analytics']
}
}
]
// src/router/routes/modules/article.ts
export const articleRoutes: RouteRecordRaw[] = [
{
path: '/article',
redirect: '/article/list',
meta: {
title: '文章管理',
icon: 'document',
requiresAuth: true
},
children: [
{
path: 'list',
name: 'ArticleList',
component: () => import('@/views/article/list.vue'),
meta: {
title: '文章列表',
keepAlive: true,
permissions: ['article:view']
}
},
{
path: 'create',
name: 'ArticleCreate',
component: () => import('@/views/article/create.vue'),
meta: {
title: '创建文章',
permissions: ['article:create']
}
},
{
path: 'edit/:id',
name: 'ArticleEdit',
component: () => import('@/views/article/edit.vue'),
meta: {
title: '编辑文章',
hidden: true, // 不在菜单显示
permissions: ['article:edit']
}
}
]
}
]
4.2 静态路由配置
typescript
// src/router/routes/static.ts
export const staticRoutes: RouteRecordRaw[] = [
// 登录页
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录',
requiresAuth: false, // 不需要登录
hidden: true // 不在菜单显示
}
},
// 重定向页(用于刷新当前路由)
{
path: '/redirect/:path(.*)',
name: 'Redirect',
component: () => import('@/views/redirect/index.vue'),
meta: {
hidden: true,
requiresAuth: false
}
},
// 错误页面
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/error/404.vue'),
meta: {
title: '页面不存在',
requiresAuth: false,
hidden: true
}
},
{
path: '/500',
name: 'ServerError',
component: () => import('@/views/error/500.vue'),
meta: {
title: '服务器错误',
requiresAuth: false,
hidden: true
}
},
{
path: '/403',
name: 'Forbidden',
component: () => import('@/views/error/403.vue'),
meta: {
title: '无权限访问',
requiresAuth: false,
hidden: true
}
},
// 捕获所有未匹配的路由
{
path: '/:pathMatch(.*)*',
redirect: '/404'
}
]
4.3 路由配置入口
typescript
// src/router/routes/index.ts
import type { RouteRecordRaw } from 'vue-router'
import { staticRoutes } from './static'
import { dashboardRoutes } from './modules/dashboard'
import { articleRoutes } from './modules/article'
import { userRoutes } from './modules/user'
// 静态路由(始终存在)
export const constantRoutes: RouteRecordRaw[] = [
...staticRoutes
]
// 异步路由(根据权限动态添加)
export const asyncRoutes: RouteRecordRaw[] = [
...dashboardRoutes,
...articleRoutes,
...userRoutes
]
// 所有路由(用于开发调试)
export const allRoutes: RouteRecordRaw[] = [
...constantRoutes,
...asyncRoutes
]
这样设计的好处:
- 按需加载:根据用户权限动态添加路由
- 代码分割:每个模块独立,便于维护
- 权限隔离:敏感路由不会暴露给无权限用户
五、专业级进度条实现
5.1 NProgress 深度配置
typescript
// src/utils/nprogress.ts
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
/**
* NProgress 完整配置详解
*
* NProgress 是一个轻量级的进度条库,专门用于页面加载指示
* 在企业级项目中,我们需要精细控制它的行为
*/
// 1. 自定义样式注入
const injectCustomStyles = (): void => {
// 避免重复注入
if (document.getElementById('nprogress-custom-styles')) return
const style = document.createElement('style')
style.id = 'nprogress-custom-styles'
style.textContent = `
/* 进度条容器 */
#nprogress {
pointer-events: none; /* 允许点击穿透 */
position: relative;
z-index: 999999; /* 确保在最顶层 */
}
/* 进度条本身 */
#nprogress .bar {
background: linear-gradient(
90deg,
#1890ff 0%, /* 起始颜色 - 蓝色 */
#52c41a 33%, /* 中间颜色 - 绿色 */
#faad14 66%, /* 中间颜色 - 黄色 */
#f5222d 100% /* 结束颜色 - 红色 */
) !important;
height: 3px !important;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 999999;
/* 光泽效果 */
box-shadow: 0 0 10px #1890ff,
0 0 5px #1890ff,
0 0 3px rgba(24, 144, 255, 0.5);
}
/* 进度条右侧的光点 */
#nprogress .peg {
display: block !important;
position: absolute;
right: 0;
width: 100px;
height: 100%;
opacity: 1;
transform: rotate(3deg) translate(0px, -4px);
box-shadow: 0 0 10px #1890ff,
0 0 5px #1890ff !important;
}
/* 隐藏旋转动画(推荐隐藏,更简洁) */
#nprogress .spinner {
display: none !important;
}
/* 移动端适配 */
@media (max-width: 768px) {
#nprogress .bar {
height: 2px !important;
}
}
/* 深色主题适配 */
@media (prefers-color-scheme: dark) {
#nprogress .bar {
background: linear-gradient(
90deg,
#177ddc 0%,
#49aa19 33%,
#d89614 66%,
#d32029 100%
) !important;
}
}
/* 打印时隐藏 */
@media print {
#nprogress {
display: none !important;
}
}
`
document.head.appendChild(style)
}
// 2. NProgress 配置详解
export const initNProgress = () => {
// 注入自定义样式
injectCustomStyles()
// 配置 NProgress
NProgress.configure({
/**
* minimum: 初始最小值
* - 默认值:0.08
* - 作用:避免进度条一开始就显示过高
* - 建议:0.08-0.15 之间,值越小"起步"越慢
*/
minimum: 0.12,
/**
* easing: 动画缓动函数
* - 可选值:'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'
* - 作用:控制进度条动画的加速度
* - 建议:'ease' 最自然
*/
easing: 'ease',
/**
* speed: 动画速度(毫秒)
* - 默认值:200
* - 作用:进度条从当前值变化到目标值的时间
* - 建议:200-400,太慢显得卡顿,太快用户看不清
*/
speed: 280,
/**
* trickle: 是否启用自动递增
* - 默认值:true
* - 作用:当进度卡住时自动缓慢前进
* - 建议:true,避免用户以为卡死了
*/
trickle: true,
/**
* trickleSpeed: 自动递增间隔(毫秒)
* - 默认值:200
* - 作用:每多少毫秒自动前进一点
* - 建议:200-300,太快显得假,太慢没效果
*/
trickleSpeed: 200,
/**
* trickleRate: 每次递增的幅度
* - 默认值:0.02
* - 作用:每次自动前进多少百分比
* - 注意:官网文档未明确说明,但源码中有
*/
// trickleRate: 0.02, // 使用默认值即可
/**
* showSpinner: 是否显示旋转动画
* - 默认值:true
* - 作用:右上角显示一个环形加载动画
* - 建议:false,大多数现代网站都隐藏了
*/
showSpinner: false,
/**
* parent: 挂载的父元素
* - 默认值:'body'
* - 作用:进度条插入到哪个DOM元素中
* - 建议:保持默认,除非有特殊需求
*/
parent: 'body',
/**
* template: 自定义HTML模板
* - 默认值:包含 bar 和 spinner 的模板
* - 作用:完全自定义进度条的结构
* - 注意:必须包含 role="bar" 的元素
*/
template: `
<div class="bar" role="bar">
<div class="peg"></div>
</div>
<div class="spinner" role="spinner">
<div class="spinner-icon"></div>
</div>
`,
// 选择器配置(与template对应)
barSelector: '[role="bar"]',
spinnerSelector: '[role="spinner"]'
})
return NProgress
}
// 3. 进度条管理器(高级功能)
export class ProgressManager {
private static instance: ProgressManager
private delayTimer: number | null = null
private startTime: number | null = null
private isStarted = false
// 单例模式
static getInstance(): ProgressManager {
if (!ProgressManager.instance) {
ProgressManager.instance = new ProgressManager()
}
return ProgressManager.instance
}
/**
* 开始进度条(带防抖)
* @param delay 延迟毫秒数,避免快速跳转时的闪烁
*/
start(delay = 150): ProgressManager {
this.clearDelay()
this.delayTimer = window.setTimeout(() => {
if (!NProgress.isStarted()) {
this.isStarted = true
this.startTime = Date.now()
NProgress.start()
}
}, delay)
return this
}
/**
* 智能完成进度条
* @param force 是否强制立即完成
*/
done(force = false): ProgressManager {
this.clearDelay()
if (this.startTime) {
const duration = Date.now() - this.startTime
// 智能判断:加载时间很短时延迟完成,避免闪烁
if (duration < 300 && !force) {
setTimeout(() => {
NProgress.done()
this.isStarted = false
}, 100)
} else {
NProgress.done(force)
this.isStarted = false
}
this.startTime = null
} else {
NProgress.done(force)
this.isStarted = false
}
return this
}
/**
* 设置具体进度
* @param amount 进度值(0-1之间)
*/
set(amount: number): ProgressManager {
NProgress.set(amount)
return this
}
/**
* 增加进度
* @param amount 增加的量(默认随机)
*/
inc(amount?: number): ProgressManager {
NProgress.inc(amount)
return this
}
/**
* 获取当前状态
*/
status(): number | null {
return NProgress.status
}
/**
* 是否已开始
*/
isStarted(): boolean {
return this.isStarted
}
/**
* 完全移除进度条DOM
*/
remove(): ProgressManager {
NProgress.remove()
this.isStarted = false
return this
}
/**
* 模拟长时间加载(用于演示或测试)
*/
simulateLongLoading(duration = 2000): ProgressManager {
this.start()
let progress = 0
const interval = setInterval(() => {
progress += 0.1
this.set(Math.min(progress, 0.9))
if (progress >= 1) {
clearInterval(interval)
setTimeout(() => this.done(), 200)
}
}, duration / 10)
return this
}
// 私有方法:清除延迟计时器
private clearDelay(): void {
if (this.delayTimer) {
clearTimeout(this.delayTimer)
this.delayTimer = null
}
}
}
// 4. 导出单例实例
export const progress = ProgressManager.getInstance()
5.2 进度条配置项详解表
| 配置项 | 默认值 | 推荐值 | 作用说明 | 使用场景 |
|---|---|---|---|---|
minimum |
0.08 | 0.1-0.15 | 进度条起始值 | 控制"起步"速度 |
easing |
'ease' | 'ease' | 动画缓动函数 | 使动画更自然 |
speed |
200 | 250-350 | 动画持续时间 | 平衡速度和流畅度 |
trickle |
true | true | 自动缓慢递增 | 避免用户以为卡死 |
trickleSpeed |
200 | 200-300 | 自动递增间隔 | 控制"假进度"速度 |
showSpinner |
true | false | 显示旋转动画 | 现代网站趋势是隐藏 |
parent |
'body' | 'body' | 挂载的父元素 | 一般不需要改 |
六、完整路由实例配置
6.1 主路由文件
typescript
// src/router/index.ts
import { createRouter, createWebHistory, type Router } from 'vue-router'
import type { RouteLocationNormalized, NavigationGuardNext } from 'vue-router'
import { constantRoutes } from './routes'
import { progress } from '@/utils/nprogress'
import { useUserStore } from '@/stores/user'
import { usePermissionStore } from '@/stores/permission'
// 白名单:不需要登录就可以访问的页面
const WHITE_LIST = new Set([
'/login',
'/404',
'/500',
'/403',
'/forgot-password',
'/register'
])
/**
* 创建路由实例
* 这是整个路由系统的核心
*/
export const createAppRouter = (): Router => {
const router = createRouter({
// 使用 HTML5 History 模式
history: createWebHistory(import.meta.env.BASE_URL),
// 初始路由(静态路由)
routes: constantRoutes,
// 滚动行为控制
scrollBehavior(to, from, savedPosition) {
/**
* 滚动行为的重要性:
* 1. 保持用户滚动位置
* 2. 新页面滚动到顶部
* 3. 锚点定位
*/
// 1. 如果有保存的位置,恢复位置(浏览器前进后退)
if (savedPosition) {
return savedPosition
}
// 2. 如果有锚点,滚动到锚点
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth', // 平滑滚动
top: 80 // 考虑固定头部的高度
}
}
// 3. 如果是新页面,滚动到顶部
return {
top: 0,
left: 0,
behavior: 'smooth'
}
}
})
// ==================== 路由守卫 ====================
/**
* 全局前置守卫
* 在路由跳转前执行,用于权限验证
*/
router.beforeEach(async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
console.log(`路由跳转: ${from.path} -> ${to.path}`)
// 1. 开始进度条(带150ms延迟,避免快速跳转时的闪烁)
progress.start(150)
// 2. 设置页面标题
updateDocumentTitle(to)
// 3. 获取用户和权限 store
const userStore = useUserStore()
const permissionStore = usePermissionStore()
// 4. 检查是否在白名单中
if (WHITE_LIST.has(to.path)) {
return next()
}
// 5. 检查登录状态(token是否存在)
if (!userStore.token) {
console.warn('未登录,跳转到登录页')
// 保存目标地址,登录后跳转回来
const redirectUrl = encodeURIComponent(to.fullPath)
return next({
path: '/login',
query: {
redirect: redirectUrl,
reason: '未登录'
}
})
}
// 6. 已登录用户访问登录页 → 跳转到首页
if (to.path === '/login') {
console.log('已登录用户访问登录页,跳转到首页')
return next(from.path || '/dashboard')
}
// 7. 检查用户信息是否已加载
if (!userStore.userInfo) {
console.log('用户信息未加载,开始获取...')
try {
// 获取用户信息
await userStore.fetchUserInfo()
// 根据用户权限生成动态路由
await permissionStore.generateRoutes(userStore.roles)
// 添加动态路由到路由器
permissionStore.dynamicRoutes.forEach(route => {
router.addRoute(route)
})
console.log('动态路由添加完成,重新跳转')
// 重新跳转到目标路由,确保新路由生效
return next({ ...to, replace: true })
} catch (error) {
console.error('获取用户信息失败:', error)
// 清除用户信息,跳转到登录页
userStore.logout()
return next({
path: '/login',
query: {
redirect: to.fullPath,
reason: '获取用户信息失败'
}
})
}
}
// 8. 检查页面访问权限
if (to.meta?.roles || to.meta?.permissions) {
const hasPermission = permissionStore.checkPermission(
to.meta.roles,
to.meta.permissions
)
if (!hasPermission) {
console.warn(`无权限访问: ${to.path}`)
return next('/403')
}
}
// 9. 所有检查通过,允许导航
next()
})
/**
* 全局后置钩子
* 路由跳转完成后执行
*/
router.afterEach((to, from) => {
console.log(`路由完成: ${from.path} -> ${to.path}`)
// 1. 完成进度条
progress.done()
// 2. 页面访问统计(可以接入Google Analytics等)
trackPageView(to.fullPath)
// 3. 发送页面切换事件(供其他组件监听)
window.dispatchEvent(new CustomEvent('route-changed', {
detail: { from, to }
}))
})
/**
* 路由错误处理
* 捕获导航过程中的错误
*/
router.onError((error, to, from) => {
console.error('路由错误:', error)
// 1. 结束进度条
progress.done()
// 2. 上报错误到监控系统
reportError(error, { to, from })
// 3. 根据错误类型处理
if (error.name === 'ChunkLoadError') {
// 组件加载失败(通常是网络问题)
router.push({
path: '/network-error',
query: {
chunk: error.message.match(/Loading chunk (\d+) failed/)?.[1],
from: from.fullPath
}
})
} else if (error.message.includes('Navigation cancelled')) {
// 导航被取消(通常是权限问题)
console.log('导航被用户取消')
} else {
// 其他未知错误
router.push('/500')
}
})
// ==================== 工具函数 ====================
/**
* 更新页面标题
*/
function updateDocumentTitle(to: RouteLocationNormalized): void {
const title = to.meta?.title as string | undefined
const appName = import.meta.env.VITE_APP_NAME || '智能博客管理系统'
if (title) {
document.title = `${title} - ${appName}`
} else {
document.title = appName
}
}
/**
* 页面访问统计
*/
function trackPageView(path: string): void {
// 生产环境才统计
if (import.meta.env.PROD) {
// 可以在这里集成各种统计工具
// 1. Google Analytics
if (window.gtag) {
window.gtag('config', 'GA_MEASUREMENT_ID', {
page_path: path
})
}
// 2. 百度统计
if (window._hmt) {
window._hmt.push(['_trackPageview', path])
}
// 3. 自定义统计(发送到自己的服务器)
fetch('/api/analytics/pageview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path,
timestamp: Date.now(),
userAgent: navigator.userAgent
})
}).catch(() => { /* 静默失败 */ })
}
}
/**
* 错误上报
*/
function reportError(error: Error, context?: any): void {
if (import.meta.env.PROD) {
// 1. Sentry(流行的错误监控)
if (window.Sentry) {
window.Sentry.captureException(error, { extra: context })
}
// 2. 自定义错误上报
const errorData = {
type: 'router_error',
message: error.message,
stack: error.stack,
context,
url: window.location.href,
timestamp: new Date().toISOString()
}
// 使用 navigator.sendBeacon,即使页面关闭也会发送
navigator.sendBeacon('/api/error/report', JSON.stringify(errorData))
}
}
// ==================== 路由工具方法 ====================
/**
* 刷新当前路由
* 企业级项目中常用的功能
*/
router.refreshCurrentRoute = function() {
const { fullPath } = router.currentRoute.value
router.replace({
path: '/redirect' + fullPath
}).then(() => {
console.log('路由刷新完成')
})
}
/**
* 跳转到登录页
* 统一的登录跳转方法
*/
router.toLogin = function(redirect?: string) {
const target = redirect || router.currentRoute.value.fullPath
router.push({
path: '/login',
query: {
redirect: encodeURIComponent(target),
t: Date.now() // 防止缓存
}
})
}
/**
* 检查是否有权限访问某个路由
*/
router.hasPermission = function(routeName: string): boolean {
const route = router.getRoutes().find(r => r.name === routeName)
if (!route) return false
const permissionStore = usePermissionStore()
return permissionStore.checkPermission(
route.meta?.roles,
route.meta?.permissions
)
}
return router
}
// 创建并导出路由实例
const router = createAppRouter()
export default router
6.2 路由守卫分离
typescript
// src/router/guards/auth.ts
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'
/**
* 认证守卫
* 负责检查用户登录状态
*/
export const authGuard = async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
): Promise<void> => {
const userStore = useUserStore()
// 白名单路径
const whiteList = ['/login', '/404', '/500', '/403']
// 在白名单中,直接放行
if (whiteList.includes(to.path)) {
return next()
}
// 检查是否有 token
if (!userStore.token) {
return next({
path: '/login',
query: {
redirect: encodeURIComponent(to.fullPath),
reason: '未登录'
}
})
}
// 已登录用户访问登录页
if (to.path === '/login') {
return next(from.path || '/')
}
next()
}
// src/router/guards/permission.ts
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { usePermissionStore } from '@/stores/permission'
/**
* 权限守卫
* 负责检查用户是否有权限访问页面
*/
export const permissionGuard = async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
): Promise<void> => {
const permissionStore = usePermissionStore()
// 如果路由不需要权限检查,直接放行
if (!to.meta?.roles && !to.meta?.permissions) {
return next()
}
// 检查权限
const hasPermission = permissionStore.checkPermission(
to.meta.roles,
to.meta.permissions
)
if (!hasPermission) {
// 无权限,跳转到403页面
return next('/403')
}
next()
}
七、布局组件与路由缓存
7.1 智能布局组件
vue
<!-- src/layouts/MainLayout.vue -->
<template>
<div class="main-layout" :class="layoutClasses">
<!-- 侧边栏(根据条件显示) -->
<AppSidebar
v-if="showSidebar"
:collapse="sidebarCollapse"
/>
<!-- 主要内容区域 -->
<div class="main-container" :style="containerStyles">
<!-- 顶部导航栏 -->
<AppHeader
v-if="showHeader"
@toggle-sidebar="toggleSidebar"
/>
<!-- 标签页导航(可选的) -->
<TagsView v-if="showTagsView" />
<!-- 应用主体 -->
<div class="app-main">
<!-- 路由视图 - 核心部分 -->
<router-view v-slot="{ Component, route }">
<!-- 页面切换过渡动画 -->
<transition
:name="transitionName"
mode="out-in"
@before-enter="onBeforeEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
>
<!-- keep-alive 缓存 -->
<keep-alive :include="cachedViews" :max="10">
<component
:is="Component"
:key="resolveComponentKey(route)"
v-if="shouldRenderComponent(route)"
/>
</keep-alive>
</transition>
</router-view>
</div>
<!-- 页面底部 -->
<AppFooter v-if="showFooter" />
</div>
<!-- 全局回到顶部 -->
<BackToTop v-if="showBackToTop" />
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, type Component } from 'vue'
import { useRoute, useRouter, type RouteLocationNormalized } from 'vue-router'
import { useAppStore } from '@/stores/app'
import { useTagsStore } from '@/stores/tags'
import AppSidebar from './components/Sidebar.vue'
import AppHeader from './components/Header.vue'
import AppFooter from './components/Footer.vue'
import TagsView from './components/TagsView.vue'
import BackToTop from './components/BackToTop.vue'
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const tagsStore = useTagsStore()
// ==================== 响应式状态 ====================
// 侧边栏折叠状态
const sidebarCollapse = ref(false)
// ==================== 计算属性 ====================
// 布局类名
const layoutClasses = computed(() => ({
'full-screen': isFullScreen.value,
'hide-sidebar': !showSidebar.value,
'hide-header': !showHeader.value,
'mobile': appStore.isMobile
}))
// 容器样式
const containerStyles = computed(() => {
if (isFullScreen.value) return { marginLeft: '0' }
return {
marginLeft: sidebarCollapse.value ? '64px' : '200px',
transition: 'margin-left 0.3s ease'
}
})
// 是否全屏模式(数据大屏等页面)
const isFullScreen = computed(() => route.meta?.fullScreen === true)
// 是否显示侧边栏
const showSidebar = computed(() => {
if (isFullScreen.value) return false
if (route.meta?.hiddenSidebar) return false
return true
})
// 是否显示顶部导航
const showHeader = computed(() => {
if (isFullScreen.value) return false
if (route.meta?.hiddenHeader) return false
return true
})
// 是否显示底部
const showFooter = computed(() => {
if (isFullScreen.value) return false
if (route.meta?.hiddenFooter) return false
return true
})
// 是否显示标签页
const showTagsView = computed(() => {
if (isFullScreen.value) return false
return tagsStore.showTagsView
})
// 是否显示回到顶部按钮
const showBackToTop = computed(() => {
return route.meta?.showBackToTop !== false
})
// 页面过渡动画名称
const transitionName = computed(() => {
return route.meta?.transition || 'fade-transform'
})
// 需要缓存的组件名称列表
const cachedViews = computed(() => {
return tagsStore.cachedViews
.filter(viewName => {
// 只缓存标记了 keepAlive 的路由
const route = router.getRoutes().find(r => r.name === viewName)
return route?.meta?.keepAlive === true
})
})
// ==================== 方法 ====================
/**
* 解析组件的缓存key
* 解决同一路由不同参数时的缓存问题
*/
const resolveComponentKey = (route: RouteLocationNormalized): string => {
// 如果有自定义的key,使用自定义key
if (route.meta?.cacheKey) {
return route.meta.cacheKey as string
}
// 否则使用路由全路径作为key
return route.fullPath
}
/**
* 判断是否应该渲染组件
* 可以在这里实现路由级别的权限控制
*/
const shouldRenderComponent = (route: RouteLocationNormalized): boolean => {
// 默认都渲染
return true
}
/**
* 切换侧边栏折叠状态
*/
const toggleSidebar = (): void => {
sidebarCollapse.value = !sidebarCollapse.value
appStore.setSidebarCollapse(sidebarCollapse.value)
}
// ==================== 生命周期钩子 ====================
// 页面进入前的回调
const onBeforeEnter = (): void => {
console.log('页面进入动画开始')
// 可以在这里触发一些进入动画
}
// 页面进入后的回调
const onAfterEnter = (): void => {
console.log('页面进入动画结束')
// 可以在这里触发一些进入后的操作
}
// 页面离开前的回调
const onBeforeLeave = (): void => {
console.log('页面离开动画开始')
// 可以在这里保存页面状态
}
// 监听路由变化
watch(
() => route.fullPath,
(newPath, oldPath) => {
console.log(`路由变化: ${oldPath} -> ${newPath}`)
// 添加标签页
if (route.name && route.meta?.title) {
tagsStore.addView({
name: route.name as string,
title: route.meta.title as string,
path: route.fullPath
})
}
// 更新页面标题(双重保证)
if (route.meta?.title) {
document.title = `${route.meta.title} - 智能博客管理系统`
}
},
{ immediate: true }
)
// ==================== 样式 ====================
// 可以在这里定义一些样式相关的逻辑
const theme = computed(() => appStore.theme)
</script>
<style scoped>
.main-layout {
display: flex;
height: 100vh;
overflow: hidden;
position: relative;
}
.main-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 100%;
transition: margin-left 0.3s ease;
overflow: hidden;
}
.app-main {
flex: 1;
position: relative;
overflow: auto;
padding: 20px;
background: #f0f2f5;
}
/* 全屏模式 */
.main-layout.full-screen {
.main-container {
margin-left: 0 !important;
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.main-layout {
flex-direction: column;
}
.main-container {
margin-left: 0 !important;
}
.app-main {
padding: 10px;
}
}
/* 页面过渡动画 */
.fade-transform-enter-active,
.fade-transform-leave-active {
transition: all 0.3s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.slide-fade-enter-active {
transition: all 0.3s ease;
}
.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(10px);
opacity: 0;
}
</style>
八、在 main.ts 中使用
typescript
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { initNProgress } from '@/utils/nprogress'
// 创建应用实例
const app = createApp(App)
// 初始化 Pinia(必须在 router 之前)
const pinia = createPinia()
app.use(pinia)
// 初始化进度条
initNProgress()
// 使用路由
app.use(router)
// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
console.error('全局错误:', err, instance, info)
// 上报错误
if (import.meta.env.PROD) {
// 发送到错误监控服务
}
}
// 全局属性(可选)
app.config.globalProperties.$router = router
app.config.globalProperties.$route = router.currentRoute
// 挂载应用
app.mount('#app')
// 开发环境调试
if (import.meta.env.DEV) {
// 将 router 实例挂载到 window,方便调试
window.$router = router
// 监听路由变化
router.afterEach((to, from) => {
console.groupCollapsed(`%c路由跳转: ${from.path} → ${to.path}`, 'color: #1890ff')
console.log('from:', from)
console.log('to:', to)
console.groupEnd()
})
}
// 全局类型声明
declare global {
interface Window {
$router: typeof router
}
}
九、总结与最佳实践
9.1 核心要点回顾
- 类型安全优先:使用 TypeScript 增强路由类型
- 模块化设计:路由配置按功能模块分离
- 权限分级:实现页面级和功能级的权限控制
- 用户体验:进度条、过渡动画、滚动行为
- 错误边界:完整的错误捕获和处理机制
- 性能优化:路由懒加载、组件缓存、代码分割
9.2 企业级特色功能
- 动态路由:根据用户权限动态添加路由
- 路由缓存:智能的 keep-alive 策略
- 进度条管理:防抖、智能完成、自定义样式
- 错误监控:集成错误上报系统
- 访问统计:页面访问数据收集
- 移动端适配:响应式布局支持
9.3 性能优化建议
- 路由懒加载 :使用
() => import()语法 - 组件缓存:合理使用 keep-alive
- 代码分割:按路由模块分割代码包
- 预加载:对重要路由进行预加载
- 滚动位置恢复:提升用户体验
9.4 扩展思路
- 微前端集成:可以作为微前端的主应用或子应用
- SSR支持:适配服务端渲染场景
- PWA支持:添加离线路由缓存
- AB测试:路由级别的功能开关
- 多语言:路由级别的国际化
通过以上配置,你的 Vue 路由系统将达到企业级标准,能够支撑复杂的业务场景和高并发访问。记住,好的路由配置不是一次性的工作,而需要随着业务发展不断迭代优化。