在 Vue 后台管理系统中,动态路由是实现权限控制的核心能力 ------ 根据后端返回的用户权限菜单,动态筛选、注册路由,而非一次性加载所有路由。本文将基于你提供的filterRoutes函数,从「原理拆解→代码实现→完整集成」全流程讲解 Vue 中动态路由的落地方式,适配 Vue2 + Vue Router3、Vue3 + Vue Router4 两大主流场景。
一、动态路由核心原理
动态路由的核心逻辑分为三步:
- 权限获取:登录后从后端获取当前用户的权限菜单(包含路由 path、标题、图标等);
- 路由过滤 :通过
filterRoutes函数对比后端菜单与本地路由表,筛选出用户可访问的路由; - 路由注册:将筛选后的路由动态添加到 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;
}
核心优化点:
- 使用
find替代双层 forEach,提升匹配效率; - 深拷贝路由对象,避免修改本地原路由;
- 递归时传入当前路由的
children,修复子路由匹配错误; - 保留本地 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>
五、关键注意事项
-
404 页面位置 :动态路由添加前,404 不能放在常量路由最后(否则会匹配所有未注册路由),需在动态路由添加完成后,通过
router.addRoute({ path: '*', redirect: '/404' })追加。 -
路由刷新失效 :刷新页面后动态路由会丢失,需在
app.vue的onMounted中重新调用getPermission加载菜单并注册路由。 -
嵌套路由匹配 :后端返回的子菜单
path需与本地子路由path一致(如父路由/system,子路由user对应/system/user)。 -
路由缓存 :若需开启
keepAlive,需在本地路由meta中配置keepAlive: true,filterRoutes会保留该配置。
六、扩展场景:侧边栏渲染
动态路由注册后,侧边栏可直接使用过滤后的路由数组渲染(以 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实现页面缓存。