视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
vue-router+vuex addRoutes实现路由动态加载及菜单动态加载
2020-11-27 22:28:43 责编:小采
文档

此案例主要实现了一个功能是,在vue实例首次运行时,在加载了login和404两个路由规则,登录成功后,根据登录用户角色权限获取该角色相应菜单权限,生成新的路由规则添加进去。

做过后台管理系统都一定做过这个功能,在对菜单权限进行粗粒度权限控制的时候,通过角色获取菜单后,异步生成菜单,所以一开始拿到需求的时候,我也以为这和平常的没什么不同,不过做起来就发现了很多问题,

1.vue-router的实例,在new vue实例的时候,就加载了,且必须加载,这个时候,登录路由一定要加载,可是这个时候没有登录,无法确定权限
2.路由规则与菜单的同步

解决思路演化,菜单和路由同步,肯定是采用了vuex,一开始的思路的是,在一开始,就把所有的路由规则加载,然后在登录的时候,取得权限路由,对比两个路由,通过修改修改一个权限字段来隐藏菜单,如果在后台页面添加了新菜单规则,路由是按模块加载的不同的文件,这时对路由的文件进行新的读写,虽然可以解决问题,但是如果手动在浏览器地址上路由,依然可以访问,所以在路由的全局钩子上还要做拦截。

这个解决方案虽然解决,但是显的比较复杂,于是就想需找新的方法,重新浏览官方api,发现在2.2.0以后,官方新增了api,addRoutes,专门针对服务端渲染路由,那么这下问题就比较简单了,下面列出实现代码。以下代码不能直接复用,需要根据实际情况修改,只是提供思路

app.js

let permission = JSON.parse(window.sessionStorage.getItem('permission')) 
if (permission) { 
 store.commit(ADD_MENU, permission) 
 router.addRoutes(store.state.menu.items) 
} 
router.beforeEach((route, redirect, next) => { 
 if (state.app.device.isMobile && state.app.sidebar.opened) { 
 store.commit(TOGGLE_SIDEBAR, false) 
 } 
 if (route.path === '/login') { 
 window.sessionStorage.removeItem('user') 
 window.sessionStorage.removeItem('permission') 
 store.commit(ADD_MENU, []) 
 } 
 let user = JSON.parse(window.sessionStorage.getItem('user')) 
 if (!user && route.path !== '/login') { 
 next({ path: '/login' }) 
 } else { 
 if (route.name) { 
 next() 
 } else { 
 next({ path: '/nofound' }) 
 } 
 } 
}) 

登录的组件login.vue

<template> 
 <el-form :model="ruleForm2" :rules="rules2" ref="ruleForm2" label-position="left" label-width="0px" 
 class="demo-ruleForm login-container"> 
 <h3 class="title">系统登录</h3> 
 <el-form-item prop="account"> 
 <el-input type="text" v-model="ruleForm2.account" auto-complete="off" placeholder="账号"></el-input> 
 </el-form-item> 
 <el-form-item prop="checkPass"> 
 <el-input type="password" v-model="ruleForm2.checkPass" auto-complete="off" placeholder="密码"></el-input> 
 </el-form-item> 
 <el-checkbox v-model="checked" checked class="remember">记住密码</el-checkbox> 
 <el-form-item style="width:100%;"> 
 <el-button type="primary" style="width:100%;" @click.native.prevent="handleSubmit2" :loading="logining">登录 
 </el-button> 
 <!--<el-button @click.native.prevent="handleReset2">重置</el-button>--> 
 </el-form-item> 
 </el-form> 
</template> 
 
<script> 
 import NProgress from 'nprogress' 
 import { mapActions, mapGetters } from 'vuex' 
 export default { 
 data () { 
 return { 
 logining: false, 
 ruleForm2: { 
 account: 'admin', 
 checkPass: '123456' 
 }, 
 rules2: { 
 account: [ 
 {required: true, message: '请输入账号', trigger: 'blur'} 
 // { validator: validaePass } 
 ], 
 checkPass: [ 
 {required: true, message: '请输入密码', trigger: 'blur'} 
 // { validator: validaePass2 } 
 ] 
 }, 
 checked: true 
 } 
 }, 
 computed: { 
 ...mapGetters([ 
 'menuitems', 
 'isLoadRoutes' 
 // ... 
 ]) 
 }, 
 methods: { 
 handleReset2 () { 
 this.$refs.ruleForm2.resetFields() 
 }, 
 handleSubmit2 (ev) { 
 this.$refs.ruleForm2.validate((valid) => { 
 if (valid) { 
 this.logining = true 
 NProgress.start() 
 let loginParams = {loginName: this.ruleForm2.account, password: this.ruleForm2.checkPass} 
 this.$http.post('/api/privilege/user/login', loginParams).then(resp => { 
 this.logining = false 
 NProgress.done() 
 let {message, data} = resp.data 
 
 if (message === 'fail') { 
 this.$notify({ 
 title: '错误', 
 message: message, 
 type: 'error' 
 }) 
 } else { 
 window.sessionStorage.setItem('user', JSON.stringify(data.user)) 
 window.sessionStorage.setItem('permission', JSON.stringify(data.permission)) 
 this.addMenu(data.permission) 
 if (!this.isLoadRoutes) { 
 this.$router.addRoutes(this.menuitems) 
 this.loadRoutes() 
 } 
 this.$router.push('/system/office') 
 } 
 }) 
 } else { 
 console.log('error submit!!') 
 return false 
 } 
 }) 
 }, 
 
 ...mapActions([ 
 'addMenu', 
 'loadRoutes' 
 ]) 
 } 
 } 
 
</script> 
 
<style lang="scss" scoped> 
 .login-container { 
 /*box-shadow: 0 0px 8px 0 rgba(0, 0, 0, 0.06), 0 1px 0px 0 rgba(0, 0, 0, 0.02);*/ 
 -webkit-border-radius: 5px; 
 border-radius: 5px; 
 -moz-border-radius: 5px; 
 background-clip: padding-box; 
 margin-bottom: 20px; 
 background-color: #F9FAFC; 
 margin: 180px auto; 
 border: 2px solid #8492A6; 
 width: 350px; 
 padding: 35px 35px 15px 35px; 
 
 .title { 
 margin: 0px auto 40px auto; 
 text-align: center; 
 color: #5058; 
 } 
 
 .remember { 
 margin: 0px 0px 35px 0px; 
 } 
 
 } 
</style> 

关键点解释

computed: { 
 ...mapGetters([ 
 'menuitems', 
 'isLoadRoutes' 
 // ... 
 ]) 
 }, 

这里是从vuex取得两个对象,menuitems是菜单对象,isLoadRoutes是用来判断是否是第一次登录,用来排除重复加载路由规则

...mapActions([ 
 'addMenu', 
 'loadRoutes' 
 ]) 

这里是从vuex取得两个方法,一个是添加菜单,一个更改loadRoutes的值

this.$router.addRoutes(this.menuitems) 

这是关键api,动态的向router实例中添加路由规则

menu模块的state与mutations

const state = { 
 items: [ 
 ], 
 isLoadRoutes: false 
} 
 
const mutations = { 
 [types.EXPAND_MENU] (state, menuItem) { 
 if (menuItem.index > -1) { 
 if (state.items[menuItem.index] && state.items[menuItem.index].meta) { 
 state.items[menuItem.index].meta.expanded = menuItem.expanded 
 } 
 } else if (menuItem.item && 'expanded' in menuItem.item.meta) { 
 menuItem.item.meta.expanded = menuItem.expanded 
 } 
 }, 
 [types.ADD_MENU] (state, menuItems) { 
 if (menuItems.length === 0) { 
 state.items = [] 
 } else { 
 generateMenuItems(state.items, menuItems) 
 } 
 }, 
 [types.LOAD_ROUTES] (state) { 
 state.isLoadRoutes = !state.isLoadRoutes 
 } 
} 

路由配置文件router.js

import Vue from 'vue' 
import Router from 'vue-router' 
import menuModule from 'vuex-store/modules/menu' 
Vue.use(Router) 
 
export default new Router({ 
 mode: 'hash', // Demo is living in GitHub.io, so required! 
 linkActiveClass: 'is-active', 
 scrollBehavior: () => ({ y: 0 }), 
 routes: [ 
 { 
 path: '/login', 
 component: require('../Login.vue'), 
 meta: { 
 expanded: false, 
 show: false 
 }, 
 name: 'Login' 
 }, 
 { 
 path: '/', 
 component: require('../views/Home.vue'), 
 meta: { 
 expanded: false, 
 show: false 
 }, 
 children: [ 
 { path: '/nofound', component: require('../404.vue'), name: 'NOFOUND', meta: {show: false} } 
 ] 
 }, 
 ...generateRoutesFromMenu(menuModule.state.items) 
 ] 
}) 
 
// Menu should have 2 levels. 
function generateRoutesFromMenu (menu = [], routes = []) { 
 for (let i = 0, l = menu.length; i < l; i++) { 
 let item = menu[i] 
 if (item.path) { 
 routes.push(item) 
 } 
 } 
 return routes 
} 

vuex

import Vue from 'vue' 
import Vuex from 'vuex' 
import * as actions from './actions' 
import * as getters from './getters' 
 
import menu from './modules/menu' 
 
Vue.use(Vuex) 
 
const store = new Vuex.Store({ 
 strict: true, // process.env.NODE_ENV !== 'development', 
 actions, 
 getters, 
 modules: { 
 menu 
 }, 
 mutations: { 
 } 
}) 
 
export default store 

actions

export const addMenu = ({ commit }, menuItems) => { 
 if (menuItems.length > 0) { 
 commit(types.ADD_MENU, menuItems) 
 } 
} 
 
export const loadRoutes = ({ commit }) => { 
 commit(types.LOAD_ROUTES) 
} 

getters

const menuitems = state => state.menu.items 
const isLoadRoutes = state => state.menu.isLoadRoutes 
export { 
 menuitems, 
 isLoadRoutes 
} 

mutations_type.js

export const ADD_MENU = 'ADD_MENU' 
 
export const LOAD_ROUTES = 'LOAD_ROUTES' 

因为上面的代码不能直接运行,再次梳理一下思路,

1.创建vue实例的时候,将vuex和vue-router加载,这个时候,vue-router只有登录规则和404规则

2.vuex中state管理的状态对象有,菜单对象menuitems,是否加载过路由loadRoutes ,并提供相应的getters与actions当然还有一些其他的,这里没有列举

3.然后在登录组件中,登录成功后,将服务端传回来之后,调用actions更改state.menuitems,并且中间有格式化的过程,这个过程的代码没有贴出来,主要是由于不同的表涉和服务端返回的数据不一样,,

4.然后调用addRoutes和actions更改已经加载过路由的方法

5.然后为了防止用户直接手动按f5刷新页面,这个时候会重新构建vue实例,而又没有重新登录,所以vuex里面的东西会清空,所以将登录后的数据存放在sessionStroage中,在刷新页面,重新构建vue实例的时候,会有判断

6.之后会渲染侧边栏组件,列出菜单,数据就可以根据state.menuitems来就可以了,我这里没有贴我的,实际根据自己的需求来

后面有时间会在github上上传完整代码。

下载本文
显示全文
专题