Vue addRoutes实现动态权限路由菜单的示例
本文介绍如何在Vue后台管理系统中,利用vue-router的addRoutes方法实现动态权限路由。通过后端返回权限列表,前端在登录成功后动态添加路由配置,解决单页应用刷新丢失路由的问题,并结合自定义指令实现按钮级别的权限控制,确保不同用户看到不同的导航菜单和操作界面。
需求与问题背景
需求
近期接手了一个后台管理系统的开发任务,核心需求是实现导航菜单的动态加载。系统需根据登录用户的权限差异,从后端获取不同的导航菜单数据,同时不同权限用户在可操作的界面上也存在区别。
问题
该项目技术栈采用vue、vue-router、element-ui和vuex。由于单页应用在页面加载前已实例化vue-router并注入Vue实例,导致在登录页面无法直接重新定制路由。经过调研发现,vue-router 2.0版本提供了addRoutes方法用于动态添加路由,这为解决该问题提供了可行方案。
经过调试实现了该功能,特此记录以便回顾,同时也希望能帮助到有类似需求的开发者。
动态路由实现思路
思路
首先,在本地预先配置好固定不变的路由地址,例如登录页和404页面,配置示例如下:
import Vue from 'vue'
import Router from 'vue-router'
import store from '@/vuex/store'
Vue.use(Router)
let router = new Router({
routes: [
{
path: '/login',
name: 'login',
meta: {requireAuth: false},
// 模块使用异步加载
component: (resolve) => require(['../components/login/login.vue'], resolve)
}]
})
// 拦截登录,token验证
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth === undefined) {
if (store.state.token) {
next()
} else {
next({
path: '/login'
})
}
} else {
next()
}
})
export default router
完成这些固定路由的配置后,用户才能正常访问登录页面,否则流程无法继续。
其次,关键步骤是与后端约定返回的权限菜单列表数据结构。首先分析所需的路由结构,以个人项目为例,若直接定义路由,其结构如下:
let router = new Router({
routes: [
{
path: '/login',
name: 'login',
meta: {requireAuth: false},
component: (resolve) => require(['../components/login/login.vue'], resolve)
},
{
path: '/',
redirect: '/layout'
},
{
path: '/layout',
component: (resolve) => require(['../layout.vue'], resolve),
children: [
{
path: 'index',
meta: {
type: '1', //控制是否显示隐藏 1显示,2隐藏
code: 00010001, // 后面需要控制路由高亮
title: '首页', // 菜单名称
permissonList: [] // 权限列表
}
component: (resolve) => require(['@/components/index/index.vue'], resolve)
},
{
...
}
]
}]
})
根据上述结构分析,真正需要动态配置的部分其实是/layout下的children节点。因此,后端只需返回包含所有路由信息的数组即可。

返回数据中,rootList包含一级导航列表,一级导航本身无路由功能,仅作为切换二级菜单的触发器;subList才是实际需要的路由信息。
获取权限路由信息后,需在本地对数据进行处理和组装,以生成所需的数据结构:
// 登录
login () {
let params = {
account: this.loginForm.username,
password: encrypt(this.loginForm.password)
}
this.loading = true
this.$http.post(this.$bumng + '/login', this.$HP(params))
.then((res) => {
this.loging = false
console.info('菜单列表:', res)
if (res.resultCode === this.$state_ok) {
// 合并一级菜单和二级菜单,便于显示
let menus = handleMenu.mergeSubInRoot(res.rootList, res.subList)
// 本地化处理好的菜单列表
this.saveRes({label: 'menuList', value: menus})
// 根据subList处理路由
let routes = handleMenu.mergeRoutes(res.subList)
// 本地化subList,便于在刷新页面的时候重新配置路由
this.saveRes({label: 'subList', value: res.subList})
// 防止重复配置相同路由
if (this.$router.options.routes.length <= 1) {
this.$router.addRoutes(routes)
// this.$router不是响应式的,所以手动将路由元注入路由对象
this.$router.options.routes.push(routes)
}
this.$router.replace('/layout/index')
}
})
.catch((err) => {
this.loging = false
console.error('错误:', err)
})
},
处理菜单列表和subList的核心方法为mergeSubInRoot和mergeRoutes。
const routes = [
{
path: '/',
redirect: '/layout'
},
{
path: '/layout',
component: (resolve) => require(['../layout.vue'], resolve),
children: []
}
]
export default {
/**
* 合并主菜单和子菜单
* @param: rootList [Array] 主菜单列表
* @param: subList [Array] 子菜单
* */
mergeSubInRoot (roots, subs) {
if (roots && subs) {
for (let i = 0; i < roots.length; i++) {
let rootCode = roots[i].code
roots[i].children = []
for (let j = 0; j < subs.length; j++) {
if (rootCode === subs[j].code.substring(0, 4)) {
roots[i].children.push(subs[j])
}
}
}
}
return roots
},
/**
* 合并远程路由到本地路由
* @param: subList [Array] 远程路由列表
* @param: routes [Array] 本地路由列表
* */
mergeRoutes (subs) {
if (subs) {
for (let i = 0; i < subs.length; i++) {
let temp = {
path: subs[i].actUrl,
name: subs[i].actUrl,
component: (resolve) => require([`@/components/${subs[i].component}.vue`], resolve),
meta: {
type: subs[i].type,
code: subs[i].code,
title: subs[i].name,
permissionList: subs[i].permissionList
}
}
routes[1].children.push(temp)
}
}
return routes
}
}
至此,权限路由已成功配置到本地路由中,系统登录后界面如下:

后续优化与权限控制
后续优化
1、菜单列表的显示及二级导航切换逻辑:
<template>
<div class="mainMenu">
<el-menu
class="menubar"
mode="horizontal"
:default-active="activeCode"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b">
<el-menu-item :index="item.code | splitCode" v-for="item in menuList" :key="item.code" @click="switchSubMenu(item)" v-if="item.code !== '0008'">
<i :class="`iconfont icon-${item.imgUrl}`"></i>
<span slot="title">{{item.name}}</span>
</el-menu-item>
</el-menu>
</div>
</template>
<script type="text/ecmascript-6">
import {mapState, mapMutations} from 'vuex'
export default {
name: 'menu',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
computed: {
...mapState(['menuList']),
activeCode () {
// 通过code保证在切换字路由的情况下一级路由也是高亮显示
return this.$route.meta.code.substring(0, 4)
}
},
methods: {
...mapMutations(['saveRes']),
// 切换二级路由
switchSubMenu (route) {
console.info('路由:', route)
if (route.actUrl !== 'index') {
// 用currentSubMenu控制二级路由数据
this.saveRes({label: 'currentSubMenu', value: route.children})
this.$router.push(`/layout/${route.children[0].actUrl}`)
} else {
// 不存在二级路由隐藏二级
this.saveRes({label: 'currentSubMenu', value: ''})
this.$router.push(`/layout/${route.actUrl}`)
}
}
},
filters: {
splitCode (code) {
return code.substring(0, 4)
}
}
}
</script>
2、防止刷新导致路由丢失。由于单页应用刷新时会重新初始化,导致所有动态配置的路由丢失,仅保留本地静态路由。此时可在app.vue(无论何处刷新,该文件均会执行)中执行以下代码:
<script>
import {decrypt} from '@/libs/AES'
import handleMenu from '@/router/handleMenu'
export default {
name: 'app',
created () {
// 当this.$router.options.routes的长度为1,且本地缓存存在菜单列表的时候才重新配置路由
if (this.$router.options.routes.length <= 1 && sessionStorage.getItem('subList')) {
let subList = JSON.parse(decrypt(sessionStorage.getItem('subList')))
let routes = handleMenu.mergeRoutes(subList)
this.$router.addRoutes(routes)
// this.$router不是响应式的,所以手动将路由元注入路由对象
this.$router.options.routes.push(routes)
}
}
}
</script>
通过上述处理,即使页面刷新,也能重新配置路由。
3、关于页面按钮级别的权限控制,可通过自定义指令实现。由于权限列表已存入相应路由的meta对象中,我们可以在每个页面轻松获取当前用户在当前页面所拥有的权限。

参考官方文档自定义指令
结语
功能实现完毕,得益于vue-router 2中addRoutes方法的引入,否则处理起来将非常棘手。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
怎么在React或Vue前端中使用Cube.js Client获取数据
2026-11-03 09:24:09
Vue.js 新手入门:怎么使用 Vite 快速搭建第一个项目
2026-11-02 10:30:55
Vue 项目中封装 heatmap.js:生命周期管理与内存释放
2026-11-02 09:17:40
uni-app 自定义组件开发与通信机制详解
2026-11-01 02:03:46
Vue.js 项目中 Mock 数据的三种实现方案
2026-10-31 17:23:18








