Vue Router 企业级配置全攻略:打造专业级路由系统

在 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
]

这样设计的好处:

  1. 按需加载:根据用户权限动态添加路由
  2. 代码分割:每个模块独立,便于维护
  3. 权限隔离:敏感路由不会暴露给无权限用户

五、专业级进度条实现

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 核心要点回顾

  1. 类型安全优先:使用 TypeScript 增强路由类型
  2. 模块化设计:路由配置按功能模块分离
  3. 权限分级:实现页面级和功能级的权限控制
  4. 用户体验:进度条、过渡动画、滚动行为
  5. 错误边界:完整的错误捕获和处理机制
  6. 性能优化:路由懒加载、组件缓存、代码分割

9.2 企业级特色功能

  • 动态路由:根据用户权限动态添加路由
  • 路由缓存:智能的 keep-alive 策略
  • 进度条管理:防抖、智能完成、自定义样式
  • 错误监控:集成错误上报系统
  • 访问统计:页面访问数据收集
  • 移动端适配:响应式布局支持

9.3 性能优化建议

  1. 路由懒加载 :使用 () => import() 语法
  2. 组件缓存:合理使用 keep-alive
  3. 代码分割:按路由模块分割代码包
  4. 预加载:对重要路由进行预加载
  5. 滚动位置恢复:提升用户体验

9.4 扩展思路

  • 微前端集成:可以作为微前端的主应用或子应用
  • SSR支持:适配服务端渲染场景
  • PWA支持:添加离线路由缓存
  • AB测试:路由级别的功能开关
  • 多语言:路由级别的国际化

通过以上配置,你的 Vue 路由系统将达到企业级标准,能够支撑复杂的业务场景和高并发访问。记住,好的路由配置不是一次性的工作,而需要随着业务发展不断迭代优化。

相关推荐
Filotimo_2 小时前
那在HTML中,action是什么
前端·okhttp·html
跟着珅聪学java2 小时前
JavaScript中编写new Vue()实例的完整教程(Vue 2.x)
前端·javascript·vue.js
Marshmallowc2 小时前
React 合成事件失效?深度解析 stopPropagation 阻止冒泡无效的原因与 React 17+ 事件委派机制
前端·javascript·react.js·面试·合成事件
孟无岐3 小时前
【Laya】LocalStorage 本地存储
typescript·游戏引擎·游戏程序·laya
遗憾随她而去.3 小时前
前端浏览器缓存深度解析:从原理到实战
前端
多仔ヾ3 小时前
Vue.js 前端开发实战之 04-Vue 开发基础(3)
vue.js
万行4 小时前
企业级前后端认证方式
前端·windows
2501_948120154 小时前
基于Vue 3的可视化大屏系统设计
前端·javascript·vue.js
+VX:Fegn08954 小时前
计算机毕业设计|基于springboot + vue酒店预订系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计