Vue 实现动态路由

在 Vue 后台管理系统中,动态路由是实现权限控制的核心能力 ------ 根据后端返回的用户权限菜单,动态筛选、注册路由,而非一次性加载所有路由。本文将基于你提供的filterRoutes函数,从「原理拆解→代码实现→完整集成」全流程讲解 Vue 中动态路由的落地方式,适配 Vue2 + Vue Router3、Vue3 + Vue Router4 两大主流场景。

一、动态路由核心原理

动态路由的核心逻辑分为三步:

  1. 权限获取:登录后从后端获取当前用户的权限菜单(包含路由 path、标题、图标等);
  2. 路由过滤 :通过filterRoutes函数对比后端菜单与本地路由表,筛选出用户可访问的路由;
  3. 路由注册:将筛选后的路由动态添加到 Vue Router 实例中,实现「按需加载」。

你提供的filterRoutes函数是第二步的核心,我们先优化该函数,再完整集成到动态路由体系中。

二、优化版 filterRoutes 函数(兼容嵌套路由)

原函数存在「子路由匹配错误」的问题(递归时传入的是全局 routes 而非当前路由的 children),先修复并完善类型提示:

javascript

运行

复制代码
/**
 * 过滤可访问的动态路由
 * @param {Array} menu 后端返回的权限菜单数组(含path/title/icon/children)
 * @param {Array} routes 本地路由表数组(待过滤)
 * @returns {Array} 可访问的路由数组
 */
export function filterRoutes(menu = [], routes = []) {
  const canRoutes = [];
  let isFirstRoute = true; // 标记首个路由是否添加不可关闭标记

  // 遍历后端菜单
  menu.forEach(menuItem => {
    // 匹配本地路由
    const matchRoute = routes.find(route => route.path === menuItem.path);
    if (!matchRoute) return; // 无匹配路由则跳过

    // 深拷贝避免修改原路由对象
    const validRoute = { ...matchRoute };

    // 1. 合并meta信息(优先后端返回值)
    validRoute.meta = {
      ...validRoute.meta, // 保留本地原有meta(如keepAlive、hidden等)
      icon: menuItem.icon || validRoute.meta?.icon || '',
      title: menuItem.title || validRoute.meta?.title || '',
      isAffix: isFirstRoute // 首个路由标记为不可关闭
    };

    // 2. 递归处理嵌套子路由
    if (menuItem.children && menuItem.children.length > 0) {
      validRoute.children = filterRoutes(menuItem.children, matchRoute.children || []);
    }

    // 3. 重置标记(仅首个路由生效)
    if (isFirstRoute) isFirstRoute = false;

    // 4. 加入可访问路由数组
    canRoutes.push(validRoute);
  });

  return canRoutes;
}

核心优化点:

  1. 使用find替代双层 forEach,提升匹配效率;
  2. 深拷贝路由对象,避免修改本地原路由;
  3. 递归时传入当前路由的children,修复子路由匹配错误;
  4. 保留本地 meta 原有配置,避免覆盖核心属性。

三、Vue2 + Vue Router3 完整实现

步骤 1:定义本地路由表(router/index.js)

将路由分为「常量路由」(无需权限,如登录页)和「异步路由」(需权限,如首页、系统管理):

javascript

运行

复制代码
import Vue from 'vue';
import Router from 'vue-router';
import { filterRoutes } from '@/utils/routeHelper'; // 引入过滤函数

Vue.use(Router);

// 1. 常量路由(所有用户可见)
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    meta: { hidden: true } // 侧边栏隐藏
  },
  {
    path: '/404',
    component: () => import('@/views/error/404'),
    meta: { hidden: true }
  }
];

// 2. 异步路由(需权限过滤)
export const asyncRoutes = [
  {
    path: '/dashboard',
    component: () => import('@/views/dashboard/index'),
    meta: { title: '首页', icon: 'dashboard' }
  },
  {
    path: '/system',
    component: () => import('@/views/layout/index'), // 布局组件
    meta: { title: '系统管理', icon: 'system' },
    children: [
      {
        path: 'user',
        component: () => import('@/views/system/user/index'),
        meta: { title: '用户管理', icon: 'user' }
      },
      {
        path: 'role',
        component: () => import('@/views/system/role/index'),
        meta: { title: '角色管理', icon: 'role' }
      }
    ]
  },
  // 404必须放在最后(动态路由添加后需重新定位)
  { path: '*', redirect: '/404', meta: { hidden: true } }
];

// 3. 创建路由实例(仅加载常量路由)
const createRouter = () => new Router({
  scrollBehavior: () => ({ y: 0 }), // 路由切换滚动到顶部
  routes: constantRoutes
});

const router = createRouter();

// 4. 动态添加路由方法(核心)
export function addDynamicRoutes(menuList) {
  // 过滤可访问路由
  const accessibleRoutes = filterRoutes(menuList, asyncRoutes);
  // 动态添加到路由实例
  accessibleRoutes.forEach(route => router.addRoute(route));
  return accessibleRoutes;
}

// 5. 重置路由(退出登录时使用)
export function resetRouter() {
  const newRouter = createRouter();
  router.matcher = newRouter.matcher; // 重置路由匹配器
}

export default router;

步骤 2:登录后获取菜单并注册路由(store/module/user.js)

通过 Vuex 管理用户权限,登录成功后请求菜单、过滤并注册路由:

javascript

运行

复制代码
import { login, getMenuList } from '@/api/user';
import { addDynamicRoutes, resetRouter } from '@/router';

const state = {
  token: localStorage.getItem('token') || '',
  menuList: [] // 后端返回的权限菜单
};

const mutations = {
  SET_TOKEN: (state, token) => {
    state.token = token;
    localStorage.setItem('token', token);
  },
  SET_MENULIST: (state, menuList) => {
    state.menuList = menuList;
  }
};

const actions = {
  // 登录
  login({ commit }, userInfo) {
    return new Promise(async (resolve, reject) => {
      try {
        const res = await login(userInfo);
        commit('SET_TOKEN', res.data.token);
        resolve();
      } catch (err) {
        reject(err);
      }
    });
  },

  // 获取权限菜单并注册动态路由
  getPermission({ commit, state }) {
    return new Promise(async (resolve, reject) => {
      try {
        // 1. 请求后端菜单
        const res = await getMenuList();
        const menuList = res.data;
        commit('SET_MENULIST', menuList);

        // 2. 过滤路由
        const accessibleRoutes = addDynamicRoutes(menuList);

        // 3. 返回可访问路由(供侧边栏渲染)
        resolve(accessibleRoutes);
      } catch (err) {
        reject(err);
      }
    });
  },

  // 退出登录
  logout({ commit }) {
    return new Promise(resolve => {
      localStorage.removeItem('token');
      commit('SET_TOKEN', '');
      commit('SET_MENULIST', []);
      resetRouter(); // 重置路由
      resolve();
    });
  }
};

export default {
  namespaced: true,
  state,
  mutations,
  actions
};

步骤 3:全局路由守卫(permission.js)

在路由跳转前校验权限,未登录则跳转登录页,已登录则加载动态路由:

javascript

运行

复制代码
import router from './router';
import store from './store';
import { Message } from 'element-ui';

// 白名单(无需登录即可访问)
const whiteList = ['/login', '/404'];

router.beforeEach(async (to, from, next) => {
  const token = store.state.user.token;

  // 1. 有token(已登录)
  if (token) {
    if (to.path === '/login') {
      next('/dashboard'); // 已登录跳首页
    } else {
      // 判断是否已加载菜单
      if (store.state.user.menuList.length === 0) {
        try {
          // 加载权限菜单并注册路由
          await store.dispatch('user/getPermission');
          // 重新跳转(确保动态路由生效)
          next({ ...to, replace: true });
        } catch (err) {
          // 加载失败,退出登录
          await store.dispatch('user/logout');
          Message.error('权限加载失败,请重新登录');
          next('/login');
        }
      } else {
        next(); // 已加载菜单,正常跳转
      }
    }
  } else {
    // 2. 无token(未登录)
    if (whiteList.includes(to.path)) {
      next(); // 白名单页面直接访问
    } else {
      next('/login'); // 非白名单跳登录页
    }
  }
});

步骤 4:main.js 引入权限守卫

javascript

运行

复制代码
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import './permission'; // 引入路由守卫

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');

四、Vue3 + Vue Router4 适配调整

Vue3 的路由 API 有变化,核心逻辑不变,仅需调整「路由注册」和「重置路由」方式:

1. 路由文件(router/index.js)

javascript

运行

复制代码
import { createRouter, createWebHashHistory } from 'vue-router';
import { filterRoutes } from '@/utils/routeHelper';

// 常量路由
export const constantRoutes = [
  { path: '/login', component: () => import('@/views/login/index'), meta: { hidden: true } },
  { path: '/404', component: () => import('@/views/error/404'), meta: { hidden: true } }
];

// 异步路由
export const asyncRoutes = [/* 同Vue2 */];

// 创建路由实例
const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes
});

// 动态添加路由(Vue3使用addRoute)
export function addDynamicRoutes(menuList) {
  const accessibleRoutes = filterRoutes(menuList, asyncRoutes);
  accessibleRoutes.forEach(route => {
    router.addRoute(route); // Vue3单条添加
  });
  return accessibleRoutes;
}

// 重置路由(Vue3需手动清空)
export function resetRouter() {
  router.getRoutes().forEach(route => {
    const { name } = route;
    if (name) router.removeRoute(name);
  });
  // 重新添加常量路由
  constantRoutes.forEach(route => router.addRoute(route));
}

export default router;

2. 组合式 API 中使用(如登录页)

vue

复制代码
<script setup>
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { login, getMenuList } from '@/api/user';

const store = useStore();
const router = useRouter();

// 登录方法
const handleLogin = async (userInfo) => {
  const res = await login(userInfo);
  store.commit('user/SET_TOKEN', res.data.token);
  // 获取菜单并注册路由
  const menuList = (await getMenuList()).data;
  store.commit('user/SET_MENULIST', menuList);
  addDynamicRoutes(menuList);
  // 跳首页
  router.push('/dashboard');
};
</script>

五、关键注意事项

  1. 404 页面位置 :动态路由添加前,404 不能放在常量路由最后(否则会匹配所有未注册路由),需在动态路由添加完成后,通过router.addRoute({ path: '*', redirect: '/404' })追加。

  2. 路由刷新失效 :刷新页面后动态路由会丢失,需在app.vueonMounted中重新调用getPermission加载菜单并注册路由。

  3. 嵌套路由匹配 :后端返回的子菜单path需与本地子路由path一致(如父路由/system,子路由user对应/system/user)。

  4. 路由缓存 :若需开启keepAlive,需在本地路由meta中配置keepAlive: truefilterRoutes会保留该配置。

六、扩展场景:侧边栏渲染

动态路由注册后,侧边栏可直接使用过滤后的路由数组渲染(以 Element-Plus 为例):

vue

复制代码
<template>
  <el-menu :routes="routes" />
</template>

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

const store = useStore();
// 从Vuex获取过滤后的路由
const routes = computed(() => store.state.user.menuList);
</script>

总结

基于filterRoutes函数实现 Vue 动态路由的核心是:「后端菜单匹配本地路由 → 筛选可访问路由 → 动态注册到 Router 实例」。

该方案既保证了权限控制的安全性(仅加载用户有权限的路由),又通过meta信息合并实现了菜单标题、图标、标签页固定等个性化需求,是后台管理系统的标准实现方式。

可根据实际业务扩展:

  • 增加「权限码匹配」(如通过menuItem.permission过滤按钮权限);
  • 支持路由懒加载的细粒度控制;
  • 结合keepAlive实现页面缓存。
相关推荐
姜糖编程日记2 小时前
C++——初识(2)
开发语言·前端·c++
丶乘风破浪丶2 小时前
Vue项目中判断相同请求的实现方案:从原理到实战
前端·javascript·vue.js
why技术2 小时前
如果让我站在科技从业者的角度去回看 2025 年,让我选一个词出来形容它,我会选择“vibe coding”这个词。
前端·后端·程序员
worxfr2 小时前
CSS Flexbox 布局完全指南
前端·css
0思必得02 小时前
[Web自动化] JS基础语法与数据类型
前端·javascript·自动化·html·web自动化
xiaohe06012 小时前
📦 Uni ECharts 是如何使用定制 echarts 的?一篇文章轻松掌握!
vue.js·uni-app·echarts
Hy行者勇哥2 小时前
JavaScript性能优化实战:从入门到精通
开发语言·javascript·性能优化
Dreamcatcher_AC2 小时前
前端面试高频问题解析
前端·css·html
Irene19912 小时前
JavaScript 常见算法复杂度总结(大O表示法)
javascript·算法