在开发企业级跨端应用时,固定的路由表往往无法满足复杂的权限需求。不同角色(如管理员、普通员工)需要看到不同的菜单和页面,同时还需要防范未授权访问。uni-app 基于 Vue 3 生态,结合 uni-simple-router 或框架内置的路由机制,完全可以实现动态路由注册与基于角色的权限控制。本文将带你从零搭建一套可扩展的动态路由与权限管理方案,覆盖路由拦截、菜单自动生成、跨平台差异处理以及按钮级权限控制,让你的 uni-app 应用具备企业级安全与灵活性。
一、为什么需要在 uni-app 中实现动态路由
传统的 uni-app 项目通常在 pages.json 中配置固定路由,然后跳转时通过路径硬编码。这种方式在简单应用中可以接受,但对于后台管理系统或多角色应用就暴露出几个问题:
- 权限耦合:路由定义在编译时确定,无法根据用户角色动态展示菜单或隐藏页面入口。
- 安全性不足:即使隐藏了菜单,用户仍可能通过直接输入路径访问无权限页面。
- 维护成本高:新增角色或调整权限时需修改 JSON 文件和代码逻辑,难以做到配置化管理。
动态路由的思路是:应用初始化时,根据当前用户角色从服务端拉取可访问的路由列表,动态注册到 uni-app 的路由系统中,同时结合路由守卫进行实时鉴权。这样路由配置就变成了运行时行为,权限数据可以集中存储在后端,实现灵活控制。
二、项目基础准备:依赖与目录结构
本文示例基于 uni-app 的 Vue 3 版本,使用 Pinia 作为状态管理器,uni-simple-router 作为路由增强库(可选,但提供了更接近 Vue Router 的 API)。如果你希望尽可能靠近原生方案,也可以直接利用 uni-app 的 uni.navigateTo 等 API 配合状态管理实现,但为了更好地展示守卫和动态注册,我们采用 uni-simple-router。
在 HBuilderX 中创建项目后,安装必要依赖:
npm install pinia uni-simple-router
推荐的目录结构如下:
src
├── router
│ ├── index.js # 路由实例与动态注册逻辑
│ └── permission.js # 路由守卫与权限判断
├── store
│ ├── user.js # 用户信息与角色状态
│ └── permission.js # 权限与路由状态
├── api
│ └── auth.js # 模拟获取权限接口
├── pages
│ ├── login
│ ├── dashboard
│ └── error
│ └── 403.vue
├── App.vue
└── main.js
这个结构清晰地分离了路由逻辑、状态管理和页面视图,便于团队协作。
三、路由实例创建与动态注册核心代码
首先在 router/index.js 中创建路由实例,并导出动态添加路由的方法。uni-simple-router 的用法与 Vue Router 类似,支持 addRoute 方法。
// src/router/index.js
import { createRouter } from 'uni-simple-router';
import store from '@/store';
// 基础静态路由(所有角色都需要的页面)
const constantRoutes = [
{
path: '/pages/login/index',
name: 'Login',
meta: { title: '登录' }
},
{
path: '/pages/error/403',
name: 'Forbidden',
meta: { title: '无权限' }
}
];
// 创建路由实例
const router = createRouter({
routes: constantRoutes,
// 使用hash模式,兼容小程序等
mode: 'hash',
h5: {
loading: true
},
appType: 'app' // 根据实际平台调整
});
// 动态添加路由的方法
export function addDynamicRoutes(routeList) {
routeList.forEach(route => {
// 处理嵌套路由(如果存在)
if (route.children && route.children.length) {
route.children.forEach(child => {
router.addRoute(child);
});
} else {
router.addRoute(route);
}
});
}
export default router;
这里将登录页和无权限页作为静态路由,因为它们与权限无关,必须能随时访问。动态添加的路由将在用户登录成功后由后端返回,并通过 addDynamicRoutes 注册。
服务端返回的权限数据结构示例
后端接口 /api/menus 返回当前用户的菜单及路由映射:
[
{
"path": "/pages/dashboard/index",
"name": "Dashboard",
"meta": { "title": "工作台", "icon": "dashboard", "roles": ["admin","user"] }
},
{
"path": "/pages/manage/users",
"name": "UserManage",
"meta": { "title": "用户管理", "icon": "user", "roles": ["admin"] }
},
{
"path": "/pages/settings/profile",
"name": "Profile",
"meta": { "title": "个人设置", "icon": "set", "roles": ["admin","user"] }
}
]
每个路由携带 meta.roles 数组,用于后续权限匹配。
四、权限状态管理:用 Pinia 存储角色与菜单
在 store/permission.js 中定义权限 Store,负责管理当前用户的可访问路由及菜单数据。
// src/store/permission.js
import { defineStore } from 'pinia';
import { addDynamicRoutes } from '@/router';
export const usePermissionStore = defineStore('permission', {
state: () => ({
routes: [], // 已添加的动态路由
menus: [] // 用于渲染侧边栏的菜单列表
}),
actions: {
// 生成动态路由
generateRoutes(menuData) {
const accessedRoutes = menuData.filter(route => {
// 此处可根据用户角色过滤:假设用户角色存在userStore中
const userStore = useUserStore();
const userRoles = userStore.roles;
if (route.meta && route.meta.roles) {
return route.meta.roles.some(role => userRoles.includes(role));
}
return true;
});
// 保存菜单用于渲染
this.menus = accessedRoutes;
// 注册路由
addDynamicRoutes(accessedRoutes);
this.routes = accessedRoutes;
},
// 重置权限(退出时调用)
resetPermission() {
this.routes = [];
this.menus = [];
}
}
});
这里通过检查用户角色与路由元信息中的 roles 取交集,决定是否添加该路由。用户退出后需要清空,避免下次登录残留旧权限。
五、核心守卫逻辑:拦截未授权访问
路由注册只是第一步,更重要的是在用户跳转时实时判定是否拥有目标页面的访问权限。uni-simple-router 支持全局前置守卫,我们在 router/permission.js 中编写守卫逻辑。
// src/router/permission.js
import router from './index';
import { useUserStore } from '@/store/user';
import { usePermissionStore } from '@/store/permission';
// 白名单:无需权限即可访问的路由名
const whiteList = ['Login', 'Forbidden'];
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
const permissionStore = usePermissionStore();
if (userStore.token) {
// 已登录状态
if (to.name === 'Login') {
// 已登录时访问登录页,重定向到首页
next({ name: 'Dashboard', replace: true });
} else {
// 检查是否已拉取权限路由
if (permissionStore.routes.length === 0) {
try {
// 模拟从后端获取菜单数据
const menuData = await fetchUserMenus();
permissionStore.generateRoutes(menuData);
// 动态路由添加后,需重新执行跳转以确保匹配到新路由
next({ ...to, replace: true });
} catch (error) {
console.error('获取权限菜单失败', error);
next({ name: 'Login' });
}
} else {
// 已有路由信息,检查目标路由是否存在(即是否有权限)
const hasPermission = permissionStore.routes.some(r => r.name === to.name);
if (hasPermission || whiteList.includes(to.name)) {
next();
} else {
next({ name: 'Forbidden', replace: true });
}
}
}
} else {
// 未登录状态
if (whiteList.includes(to.name)) {
next();
} else {
next({ name: 'Login', query: { redirect: to.fullPath } });
}
}
});
守卫流程清晰:首次登录后异步拉取权限菜单,动态注册路由,然后重新跳转目标页;已注册路由后,通过匹配路由名称检查权限;未登录则重定向到登录页并携带重定向参数。这种模式兼容 H5、App 及各类小程序。
模拟后端请求函数
// src/api/auth.js
export function fetchUserMenus() {
return new Promise((resolve) => {
setTimeout(() => {
// 模拟管理员角色得到的菜单
resolve([
{ path: '/pages/dashboard/index', name: 'Dashboard', meta: { title: '工作台', roles: ['admin','user'] } },
{ path: '/pages/manage/users', name: 'UserManage', meta: { title: '用户管理', roles: ['admin'] } },
{ path: '/pages/settings/profile', name: 'Profile', meta: { title: '个人设置', roles: ['admin','user'] } }
]);
}, 500);
});
}
六、侧边栏菜单自动生成与多端适配
有了 permissionStore.menus 后,我们可以轻松驱动侧边栏或底部导航栏的动态渲染。以下是一个简化的侧边栏组件示例,适用于 H5/App 端:
<template>
<view class="sidebar">
<view v-for="menu in menus" :key="menu.name" @click="navigateTo(menu)">
<text>{{ menu.meta.title }}</text>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue';
import { usePermissionStore } from '@/store/permission';
import { useRouter } from 'uni-simple-router';
const permissionStore = usePermissionStore();
const router = useRouter();
const menus = computed(() => permissionStore.menus);
function navigateTo(menu) {
router.push({ name: menu.name });
}
</script>
对于小程序,侧边栏通常转化为底部标签栏或抽屉菜单。如果使用原生的 tabBar,则需要动态设置 tabBar 列表,但注意小程序对 tabBar 的修改限制较多,推荐使用自定义 tabBar 实现动态化。
七、按钮级权限控制指令
除了页面路由,企业应用中常需要对按钮进行显隐控制。我们可以封装一个自定义指令 v-permission,传入角色数组即可控制按钮是否显示。
// src/directives/permission.js
import { useUserStore } from '@/store/user';
export default {
mounted(el, binding) {
const { value } = binding; // 例如 ['admin']
const userStore = useUserStore();
const hasPermission = userStore.roles.some(role => value.includes(role));
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el);
}
}
};
在 main.js 中全局注册:
import permissionDirective from '@/directives/permission';
app.directive('permission', permissionDirective);
组件中使用:
<button v-permission="['admin']">删除用户</button>
这种声明式控制比在模板中写 v-if 判读角色更简洁,且指令可以集中管理移除逻辑。
八、完整流程串联与测试
梳理一下完整的用户访问流程:
- 用户打开应用,
App.vue中初始化路由。 - 若本地存在 token,则直接进入守卫,拉取菜单并动态注册路由;否则跳转登录页。
- 登录成功后,存储 token 及用户角色,再次触发守卫拉取权限。
- 侧边栏基于
permissionStore.menus动态渲染,按钮通过指令受控。 - 用户尝试直接输入无权限路径时,守卫拦截并跳转到 403 页面。
为了适应不同平台,需要注意几点:
- 小程序不支持
uni-simple-router的 H5 模式,需使用原生路由跳转配合状态手动管理;但思路相同。 - 动态注册路由在小程序中需要转为维护一个路由名称与路径的映射表,配合
uni.navigateTo使用。 - 权限数据建议缓存至本地存储,避免每次打开应用都请求接口,但退出时务必清除。
九、小结与扩展方向
本文展示的动态路由与权限管理方案,将路由控制权从编译时转移到了运行时,显著提升了 uni-app 应用面对多角色场景的灵活性。核心思路同样适用于其他跨端框架。
你可以在此基础上进一步扩展:
- 数据权限:不仅控制页面,还控制 API 返回的数据范围,可在请求拦截器中基于角色注入过滤参数。
- 多级角色与部门:将角色扩展为树形,支持更复杂的权限继承。
- 可视化权限配置:开发一个管理后台,允许管理员拖拽配置角色与路由的对应关系,最终存储到数据库,供前端实时拉取。
动态路由虽带来灵活性,但也会增加前期开发复杂度。如果你的应用角色固定且页面较少,静态路由足以应对;但当应用规模增长,角色划分日益复杂时,尽早引入这套动态方案将极大降低维护成本。

