用addRoutes实现动态路由

原文转自前端路上,转载请注明出处。前端

 

以前在基于Vue实现后台系统权限控制一文中提到路由权限的实现思路,由于不喜欢在每次路由跳转的before钩子里作判断,因此在初始化Vue实例前对路由作了筛选,再用实际路由初始化Vue实例,代价是登陆页须要从Vue实例中独立出来,实现上倒没什么问题,不过这种作法须要在登陆和首页之间经过url跳转,感受老是不太”优雅”,实际上只要能在登陆后动态修改当前实例的路由就好了,以前确实没办法,但vue-router 2.2版本新增了一个router.addRoutes(routes)方法,让动态路由得以实现。vue

想固然的实现方案

用动态路由实现路由权限控制貌似是一个完美的方案,初始路由只有登陆和404,登陆后动态添加可用路由,同时将菜单数据保存到Vuex或本地用于实现动态菜单,关键节点大体以下:ios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//初始路由:
[{
path: '/login',
name: 'login',
component: (resolve) => require(['../views/common/404.vue'], resolve)
}, {
path: '/404',
name: '404',
component: (resolve) => require(['../views/common/404.vue'], resolve)
}, {
path: '*',
redirect: '/404'
}]

//登陆逻辑
let vm = this;
axios.get('/login', vm.user).then((res) => {
let extendsRoutes = filterRoutes(res.menus);
<!--
//假设获得的可用路由以下
[{
path: '/',
name: '首页',
component: (resolve) => require(['../views/index.vue'], resolve),
children: [{
path: '/menus',
name: '菜单管理',
component: (resolve) => require(['../views/menus.vue'], resolve)
}, {
path: '/resources',
name: '资源管理',
component: (resolve) => require(['../views/resources.vue'], resolve)
}]
}]-->
//存菜单
sessionStorage.setItem('menus',JSON.stringify(extendsRoutes[0].children));
//动态添加路由
vm.$router.addRoutes(extendsRoutes);
//跳转到应用界面
vm.$router.push({path:'/'});
})

//首页获取菜单数据
this.menus = JSON.parse(sessionStorage.getItem('menus'));
//用此数据循环菜单
..

目前为止看上去一切顺利,然而前方有坑。vue-router

动态路由的坑

第一个坑是,若是你将这套逻辑实现以后会发现打开应用看到的第一个页面是404,这是由于启动服务后将默认打开首页’/‘,然而初始路由中没有这个路径,所以根据路由规则跳转到了404。咱们但愿结果固然是跳转到’/login’,所以须要对这种状况作判断,在用户登陆以前全部请求都要指向’/login’,这个判断能够在before钩子里作也能够在根组件里作,建议作在根组件的created回调里,核心代码大概这样:axios

1
2
3
4
let isLogin = sessionStorage.getItem('user');
if(!isLogin){
return this.$router.push({path:'/login'});
}

这时候已经能够顺利登陆了,登陆后很快就会发现第二个坑,手动刷新页面又会跳到404,这是由于刷新会致使Vue从新实例化,路由也恢复到了初始路由,因而当前路径又被重定向到了404,这个问题的根源是可用路由没有实现持久化,那么能够经过将路由数据存sessionStorage来解决,实例化以前若是检测到本地路由就直接合并路由,像这样:session

1
2
3
4
5
6
7
8
9
10
11
//检测本地路由
let localRoutes = sessionStorage.getItem('routes');
if(localRoutes){
router.addRoutes(JSON.parse(localRoutes));
}
//实例化
new Vue({
el: '#app',
router,
render: h => h(App)
});

理论上能够,但实际操做要远比上述代码复杂,由于存在本地的只能是权限数据而不是真实路由,路由在存、取以前都要先根据权限匹配得到,过程仍是挺繁琐的,并且必须依赖sessionStorage这种持久存储,没有其余方法。问题就出在这个sessionStorage上,原则上权限只能在内存变量中流转,不能直接暴露到用户可操做的地方,试想只要用户手动修改了sessionStorage里的权限,再刷新一下页面就能突破前端路由控制了,很是的不靠谱。app

改进方案

既然不能存本地,那就每次刷新都从新从服务端获取,因此改进后的方案是本地存用户token,每次刷新要凭token从服务端从新获取用户信息和权限,而后动态更新路由,获取权限操做能够跟登陆检测一块儿放在根组件的created回调中进行,确保访问任何路径都会先执行这一步,但由于获取权限是异步操做,在此以前仍然会通过应用初始化,因此仍是会遇到404的问题,为此咱们只需作一个小调整,将不匹配路径(‘*’)跳404的路由从初始路由中移除,动态更新路由时再把这个配置加进去,以下:异步

1
2
3
4
5
6
let userPath = ...//咱们的动态路由
//注入时拼接404处理路由
this.$router.addRoutes(userPath.concat([{
path: '*',
redirect: '/404'
}]));

这样就解决了刷新问题,后面还有几个小问题就简单了。ui

首先是菜单,以前经过$router.options.routes访问路由数据实现动态菜单,但这个数据不是响应式的,没法追踪动态路由的变化,所以咱们须要将获得的导航菜单数据存到sessionStorage或Vuex里实现数据共享。this

资源权限控制也受到很大的影响,实现较为细致的权限控制须要一个自定义权限验证指令和一个全局验证方法,以前的方案里权限是在Vue实例化以前获取的,因此能够很方便的拿到权限后实现验证方法,而后用验证方法实现自定义指令,再将方法全局混合进Vue,而后实例化,这样实例中的 全部组件均可以使用自定义指令和验证方法;但如今的方案是先实例化再获取权限,实例化以前根本没有权限数据,因此自定义指没法实现,等拿到权限后实现了验证方法,却没法再全局混合了。

这个问题最后也解决了,但解决方案就完全的”有辱斯文”了,首先是全局方法的实现,直接这么作:

1
2
3
Vue.prototype.has = function(){
...
}

使用方式跟全局混合的方法彻底同样。

自定义指令的实现原本很头疼,由于全局指令只能在实例化以前实现,但那时候又确实没有权限,不过既然验证方法这么作的话,指令却是也顺便解决了,像这样:

1
2
3
4
5
6
7
8
//权限指令
Vue.directive('has', {
bind: function(el, binding) {
if (!Vue.prototype.has(binding.value)) {
el.parentNode.removeChild(el);
}
}
});

神奇的prototype貌似自带惰性效果,能够先注册后实现,具体缘由我也不太明白,如过有大牛路过,但愿能留下答案。

后记

生命不息,折腾不止啊,原本已经放弃的思路,捋着捋着居然捋顺了,而后又花了大半天把原来多入口的项目改为了单入口,虽然麻烦了一顿,但内心总算舒坦了。

相关文章
相关标签/搜索