Vue Router嵌套路由的那些坑我帮你踩过了

UP主~法霞 前端 阅读 2,137
赞 27 收藏
二维码
手机扫码查看
反馈

一个管理后台项目的路由设计

最近做了个企业级的后台管理系统,说实话嵌套路由这块还是挺折腾的。项目需要实现多个模块的深度嵌套,比如订单管理下面还要分待支付、已支付、已完成这些子状态页,用户管理里也要细分权限设置、角色分配等功能页。一开始觉得Vue Router的嵌套路由应该挺简单的,结果实际开发中遇到了不少意料之外的问题。

Vue Router嵌套路由的那些坑我帮你踩过了

基础架构搭建

项目结构定的是典型的管理后台样式,左侧菜单,顶部导航,主内容区域动态切换。路由层级大概长这样:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/components/Layout.vue'

const routes = [
  {
    path: '/admin',
    component: Layout,
    children: [
      {
        path: 'dashboard',
        name: 'Dashboard',
        component: () => import('@/views/Dashboard.vue')
      },
      {
        path: 'orders',
        name: 'Orders',
        component: () => import('@/views/orders/OrdersLayout.vue'),
        children: [
          {
            path: '',
            redirect: 'pending'
          },
          {
            path: 'pending',
            name: 'OrdersPending',
            component: () => import('@/views/orders/Pending.vue')
          },
          {
            path: 'paid',
            name: 'OrdersPaid',
            component: () => import('@/views/orders/Paid.vue')
          },
          {
            path: 'completed',
            name: 'OrdersCompleted',
            component: () => import('@/views/orders/Completed.vue')
          }
        ]
      },
      {
        path: 'users',
        name: 'Users',
        component: () => import('@/views/users/UsersLayout.vue'),
        children: [
          {
            path: '',
            redirect: 'list'
          },
          {
            path: 'list',
            name: 'UsersList',
            component: () => import('@/views/users/List.vue')
          },
          {
            path: 'roles',
            name: 'UserRoles',
            component: () => import('@/views/users/Roles.vue')
          },
          {
            path: 'permissions',
            name: 'UserPermissions',
            component: () => import('@/views/users/Permissions.vue')
          }
        ]
      }
    ]
  }
]

Layout组件就是基本的框架布局:

<!-- components/Layout.vue -->
<template>
  <div class="admin-layout">
    <header class="top-nav">
      <!-- 顶部导航栏 -->
      <TopNav />
    </header>
    <div class="main-content">
      <aside class="sidebar">
        <!-- 左侧菜单 -->
        <SideMenu />
      </aside>
      <main class="content-area">
        <!-- 主要内容区域 -->
        <router-view />
      </main>
    </div>
  </div>
</template>

<script setup>
import TopNav from './TopNav.vue'
import SideMenu from './SideMenu.vue'
</script>

这里的关键在于每层都有独立的router-view,让不同层级的组件可以渲染不同的子路由内容。OrdersLayout和UsersLayout分别承载各自的子页面,避免所有组件都在同一层级。

踩坑最多的是keep-alive缓存

这个问题真的让我折腾了两天。用户反映切换菜单的时候页面会重新加载,之前填的数据都没了。开始还以为是数据响应式的问题,查了半天才发现是嵌套路由下keep-alive的行为和单层不太一样。

最初我是这么写的:

<!-- 错误写法 -->
<template>
  <div class="content-area">
    <keep-alive>
      <router-view />
    </keep-alive>
  </div>
</template>

这样只缓存了最外层的路由组件,内部的嵌套子路由还是会被销毁重建。后来调整成了动态include的方式:

<!-- 正确写法 -->
<template>
  <div class="content-area">
    <keep-alive :include="cachedViews">
      <router-view :key="$route.fullPath" />
    </keep-alive>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'

const store = useStore()
const cachedViews = computed(() => store.state.cachedViews)
</script>

配合Vuex来管理缓存的页面名称:

// store/modules/cache.js
const state = {
  cachedViews: []
}

const mutations = {
  ADD_CACHED_VIEW(state, view) {
    if (!state.cachedViews.includes(view.name)) {
      state.cachedViews.push(view.name)
    }
  },
  REMOVE_CACHED_VIEW(state, view) {
    const index = state.cachedViews.indexOf(view.name)
    if (index > -1) {
      state.cachedViews.splice(index, 1)
    }
  },
  CLEAR_ALL_CACHED_VIEWS(state) {
    state.cachedViews = []
  }
}

在路由守卫里添加缓存管理:

// router/index.js
router.beforeEach((to, from, next) => {
  // 添加缓存
  if (to.meta.keepAlive) {
    store.commit('ADD_CACHED_VIEW', to)
  }
  // 移除不需要缓存的
  if (from.meta.keepAlive && !to.meta.keepAlive) {
    store.commit('REMOVE_CACHED_VIEW', from)
  }
  next()
})

这里注意我踩过好几次坑的地方:嵌套路由的缓存顺序很重要,如果某个父级路由没被缓存,那么它的子路由也不会被缓存。所以要在路由meta里明确标记是否需要keepAlive。

菜单高亮和面包屑同步

另一个头疼的问题是菜单选中状态和面包屑的动态更新。嵌套层级一深,父子路由之间的状态同步就很复杂了。

我的解决方案是在SideMenu组件里监听$route的变化:

<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const activeMenu = ref('')

watch(
  () => route.path,
  (newPath) => {
    // 根据当前路径设置激活菜单
    setActiveMenu(newPath)
  },
  { immediate: true }
)

const setActiveMenu = (path) => {
  // 找到对应的菜单项
  const matched = findMenuByPath(path)
  activeMenu.value = matched ? matched.name : ''
}

const findMenuByPath = (path) => {
  // 遍历菜单数据找对应项
  return menuData.find(item => 
    path.includes(item.path) || 
    item.children?.some(child => path.includes(child.path))
  )
}
</script>

面包屑也是类似的逻辑,在Layout组件里处理:

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const breadcrumbItems = computed(() => {
  const matched = route.matched.filter(item => item.meta && item.meta.title)
  return matched.map(item => ({
    title: item.meta.title,
    path: item.path
  }))
})
</script>

权限控制和动态路由

最复杂的是结合权限验证的动态路由加载。用户的权限等级决定了能看到哪些菜单和页面,这就需要在登录成功后根据权限信息动态生成路由配置。

// utils/routeGenerator.js
export function generateRoutes(userPermission, routes) {
  const res = []
  
  routes.forEach(route => {
    const tmp = { ...route }
    
    if (hasPermission(userPermission, tmp)) {
      if (tmp.children) {
        tmp.children = generateRoutes(userPermission, tmp.children)
      }
      res.push(tmp)
    }
  })
  
  return res
}

function hasPermission(permissions, route) {
  if (route.meta && route.meta.permission) {
    return permissions.some(p => route.meta.permission.includes(p))
  }
  return true
}

登录后动态添加路由:

// store/modules/user.js
async login({ commit }, userInfo) {
  const response = await loginApi(userInfo)
  const { permissions } = response.data
  
  // 生成用户专属路由
  const userRoutes = generateRoutes(permissions, baseRoutes)
  
  // 动态添加路由
  userRoutes.forEach(route => {
    router.addRoute('AdminLayout', route)
  })
  
  commit('SET_ROUTES', userRoutes)
}

这里要注意动态添加路由的时机,必须在路由初始化之后,否则会出现找不到路由的情况。

性能优化和内存泄漏

嵌套路由层级一深,组件销毁和创建的开销就明显增加了。特别是某些页面有大量数据渲染和定时器操作,退出页面时清理不及时就会造成内存泄漏。

在每个页面组件里都加了onUnmounted钩子:

<script setup>
import { onUnmounted } from 'vue'

let timer = null

const startTimer = () => {
  timer = setInterval(() => {
    // 定时任务
  }, 5000)
}

onUnmounted(() => {
  if (timer) {
    clearInterval(timer)
    timer = null
  }
  // 其他清理工作
})
</script>

回顾与反思

这套嵌套路由系统跑到现在已经有两个月了,整体稳定性还可以。最大的收获是明白了缓存策略的重要性,以及动态路由生成的时机控制。不过还是有些地方不够完美,比如深层嵌套下的错误边界处理还不够完善,某些极端情况下还是会闪屏。这些问题暂时不影响主要功能,计划下个版本统一修复。

总的来说嵌套路由虽然复杂,但只要把各个层级的关系理清楚,配合好状态管理和缓存策略,还是能做出稳定可靠的系统的。以后类似需求会考虑更早规划好缓存机制和生命周期管理。

以上是我踩坑后的总结,希望对你有帮助。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
夏侯若惜
对于新手来说,有没有更简单易懂的执行步骤呀?
点赞
2026-03-14 15:25