前端 + 接口请求实现 vue 动态路由
在 Vue 应用中,通过前端结合后端接口请求来实现动态路由是一种常见且有效的权限控制方案。这种方法允许前端根据用户的角色和权限,动态生成和加载路由,而不是在应用启动时就固定所有的路由配置。
实现原理
定义静态路由配置:
在项目的初始阶段,定义一套完整的路由配置,这些配置包含了所有可能的路由路径和相关的权限信息。用户登录与鉴权:
用户登录时,前端向后端发送请求验证用户的身份。服务器验证成功后,返回一个包含用户信息和权限的数据对象。获取用户权限信息:
前端根据登录时获得的令牌(如 JWT),再次向后端请求获取当前用户的权限信息。权限信息可能包括用户的角色、能够访问的资源等。动态生成路由:
前端根据从后端获取的权限信息,动态生成符合用户权限的路由表。这个过程可以通过递归算法处理路由配置树,根据用户的权限过滤掉无权访问的路由。动态添加路由:
使用 Vue Router 的 router.addRoutes(routes) 方法将生成的路由动态添加到路由实例中。这样只有经过权限验证的路由才会被添加,从而实现了权限控制。动态渲染菜单:
左侧菜单通常是基于生成的路由表来渲染的,因此只有用户有权访问的路由才会在菜单中显示。优点
安全性:
只有经过验证的用户才能访问其权限范围内的页面。减少了由于硬编码路由导致的安全漏洞。灵活性:
可以根据用户的权限动态调整应用的结构,无需重新部署整个应用即可调整路由。支持按需加载(懒加载),提高应用性能。用户体验:
只展示用户可以访问的菜单项,避免显示无用链接,提高用户体验。用户界面更加简洁,只显示与其角色相关的功能。可维护性:
简化了路由配置,因为不需要为每个角色单独编写路由配置,而是集中管理权限。更容易扩展和修改权限配置,只需更新后端的权限数据即可。开发效率:
开发者只需要关注业务逻辑,而不需要关心每个角色的具体路由配置。减少了重复工作,提高了开发效率。示例
在前导航路由钩子 beforeEach
函数里发送接口请求获取路由信息
permission.js
// permission.jsimport router from './router'import store from './store'import { Message } from 'element-ui'import { getStore } from '@/utils/store';const whiteList = ['/login', '/404', '/401'];router.beforeEach((to, from, next) => { let token = getStore('token'); if (token) { /* has token*/ if (to.path === '/login') { next({ path: '/' }); } else { if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 store.dispatch('GetInfo').then(() => { // 获取路由信息 store.dispatch('GenerateRoutes').then((res) => { console.log('--------------', res); // 根据roles权限生成可访问的路由表 router.addRoutes(res) // 动态添加可访问路由表 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 }) }).catch(err => { store.dispatch('LogOut').then(() => { Message.error(err) next(`/`) }) }) } else { next() } } } else { // 没有token if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 next() } else { next(`/login`) // 否则全部重定向到登录页 } }})
permission.js 文件需引入到 main.js 里
如果项目 vue-router
版本超过 3.3.0, 需要遍历路由数组再使用 router.addRoute()
方法逐个添加路由
res.forEach( route => {router.addRoute(route);})
假设后端接口返回的路由权限如下
[ { path: '/admin', meta: { title: "系统管理", }, component: 'Layout', children: [ { path: 'user', name: 'userIndex', meta: { title: "用户管理", }, component: '/admin/user/index.vue' }, { path: 'role', name: 'roleIndex', meta: { title: "角色管理", }, component: '/admin/role/index.vue', children: [ { path: 'add', name: 'addRole', meta: { title: "添加角色", }, component: '/admin/user/index.vue' }, { path: 'update', name: 'updateRole', meta: { title: "编辑角色", }, component: '/admin/role/index.vue' } ] } ] }, { path: '/tableEcho', meta: { title: "表格管理", }, component: 'Layout', children: [ { path: 'test', name: 'tableEchoIndex', meta: { title: "表格测试", }, component: '/tableEcho/index.vue', children: [ { path: 'add', name: 'addTable', hidden: true, meta: { title: "新增测试", }, component: '/tableEcho/add.vue' } ] }, ], },]
vuex 处理数据
store/index.vue
// store/index.vueimport Vue from 'vue'import Vuex from 'vuex'import { routes, dynamicRoutes } from "@/router";import { login, getInfo, logout, getRouters } from "@/api/user";import { setStore, clearStore } from '@/utils/store';import Layout from '@/Layout/index.vue'Vue.use(Vuex)export default new Vuex.Store({ state: { routes, token: "", roleType: "", roles: [], permissions: [], sidebarRouters: [], }, getters: { token: state => state.token, roles: state => state.roles, permissions: state => state.permissions, sidebarRouters: state => state.sidebarRouters, }, mutations: { SET_TOKEN: (state, token) => { state.token = token; }, SET_USERINFO: (state, user) => { state.userInfo = user; }, SET_ROLETYPE: (state, roleType) => { state.roleType = roleType; }, SET_ROLES: (state, roles) => { state.roles = roles; }, SET_PERMISSIONS: (state, permissions) => { state.permissions = permissions; }, SET_ROUTE: (state, sidebarRouters) => { state.sidebarRouters = sidebarRouters; }, }, actions: { Login({ commit }, userInfo) { return new Promise((resolve, reject) => { login(userInfo).then(res => { setToken(res.data.token); setStore('token', res.data.token); commit('SET_TOKEN', res.data.token); resolve(); }).catch(error => { reject(error); }) }) }, // 获取用户信息 GetInfo({ commit }) { return new Promise((resolve, reject) => { getInfo().then(res => { console.log('res::: ', res); if (res.data.code === 0 || 200) { const user = res.data.sysUser; const roleType = res.data.roleType; commit('SET_USERINFO', user); // roleType 用户所用的权限级别 1 普通用户 2 项目经理 3 部门管理员 4 综合部管理员 5 部门领导 -1 项目运维管理员 setStore('ROLE_TYPE', roleType); if (res.data.roles) { // 验证返回的roles是否为真 commit('SET_ROLES', res.data.roles); commit('SET_PERMISSIONS', res.data.permissions); } else { commit('SET_ROLES', ['ROLE_DEFAULT']); } resolve(); } else { reject(error); } }).catch(error => { reject(error); }) }) }, GenerateRoutes({ commit }) { return new Promise((resolve, reject) => { // 向后端请求路由数据 getRouters().then(res => { if (res.data.code === 0 || 200) { const sdata = JSON.parse(JSON.stringify(res.data.routes)); console.log('sdata::: ', sdata); let newRouters = filterAsyncRouter(sdata); // 连接公共路由 const sidebarRoutes = routes.concat([...newRouters]); commit('SET_ROUTE', sidebarRoutes); resolve(sidebarRoutes); } else { reject(error); } }).catch(error => { reject(error); }) }) }, // 退出系统 LogOut({ commit, state }) { return new Promise((resolve, reject) => { logout(state.token).then(() => { commit('SET_TOKEN', '') commit('SET_ROLES', []) commit('SET_PERMISSIONS', []) clearStore('token'); clearStore('userInfo') resolve() }).catch(error => { reject(error) }) }) }, }, modules: { }})const loadView = (view) => { // 路由懒加载 return () => import(`@/views${view}`);};function filterAsyncRouter(routes) { return routes.filter((route) => { if (Array.isArray(route.children) && route.children.length > 0) { // 如果该路由含有子路由时,递归调用该函数 route.children = filterAsyncRouter(route.children); } if (route.component) { // Layout ParentView 组件特殊处理 if (route.component === "Layout") { route.component = Layout; } else { // 路由组件懒加载 route.component = loadView(route.component); } } return true; })}
由于接口返回的 component
是字符串, 需手动封装函数转换成组件
公共路由如下
router/index.js
// router/index.jsimport Vue from 'vue'import VueRouter from 'vue-router'import Layout from '@/Layout/index.vue'Vue.use(VueRouter)// 公共路由export const routes = [ { path: '/', name: 'redirect', component: Layout, hidden: true, // 隐藏菜单 redirect: "/homePage", // 用户在地址栏输入 '/' 时会自动重定向到 /homePage 页面 }, { path: '/homePage', component: Layout, redirect: "/homePage/index", meta: { title: "首页", }, children: [ { path: 'index', name: 'homePageIndex', meta: { title: "首页", }, component: () => import('@/views/homePage/index.vue') } ] }, { path: '/login', component: () => import('@/views/login.vue'), hidden: true }, { path: '/404', component: () => import('@/views/error/404.vue'), hidden: true }, { path: '/401', component: () => import('@/views/error/401.vue'), hidden: true },]const router = new VueRouter({ base: process.env.BASE_URL, routes})export default router
文件结构如下
页面菜单渲染
左侧菜单实现参考链接: Elemnt-UI + 递归组件实现后台管理系统左侧菜单
前端单独实现动态路由参考连接: 前端单独实现 vue 动态路由
总结
通过以上步骤,你可以实现一个完整的动态路由权限管理系统:
后端接口返回路由配置:获取用户的权限信息及路由信息。动态加载组件:使用异步组件方式加载指定路径的组件。动态添加路由:根据权限信息动态添加路由。路由守卫:使用router.addRoutes()
或 router.addRoute()
添加路由。 这样可以确保应用根据用户的权限动态加载相应的路由,增强安全性与灵活性。