在权限管理模块中,这算是前端的核心了。
9.1 核心思路
用户在登录成功之后,进入 home 主页之前,向服务端发送请求,要求获取当前的菜单信息和组件信息,服务端根据当前用户所具备的角色,以及角色所对应的资源,返回一个 json 字符串,格式如下:
[
{
"id": 2,
"path": "/home",
"component": "Home",
"name": "员工资料",
"iconCls": "fa fa-user-circle-o",
"children": [
{
"id": null,
"path": "/emp/basic",
"component": "EmpBasic",
"name": "基本资料",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
},
{
"id": null,
"path": "/emp/adv",
"component": "EmpAdv",
"name": "高级资料",
"iconCls": null,
"children": [],
"meta": {
"keepAlive": false,
"requireAuth": true
}
}
],
"meta": {
"keepAlive": false,
"requireAuth": true
}
}
]
前端在拿到这个字符串之后,做两件事:
- 将 json 动态添加到当前路由中;
- 将数据保存到 store 中,然后各页面根据 store 中的数据来渲染菜单。
核心思路并不难,下面我们来看看实现步骤。
9.2 数据请求时机
这个很重要。
可能会有小伙伴说这有何难,登录成功之后请求不就可以了吗?是的,登录成功之后,请求菜单资源是可以的,请求到之后,我们将之保存在 store 中,以便下一次使用,但是这样又会有另外一个问题,假如用户登录成功之后,点击某一个子页面,进入到子页面中,然后按了一下 F5 进行刷新,这个时候就 GG 了,因为 F5 刷新之后 store 中的数据就没了,而我们又只在登录成功的时候请求了一次菜单资源,要解决这个问题,有两种思路:
- 将菜单资源不要保存到 store 中,而是保存到 localStorage 中,这样即使 F5 刷新之后数据还在;
- 直接在每一个页面的 mounted 方法中,都去加载一次菜单资源。
由于菜单资源是非常敏感的,因此最好不要不要将其保存到本地,故舍弃方案 1,但是方案 2 的工作量有点大,因此我采取办法将之简化,采取的办法就是使用路由中的导航守卫。
9.3 路由导航守卫
我的具体实现是这样的,首先在 store 中创建一个 routes 数组,这是一个空数组,然后开启路由全局守卫,如下:
router.beforeEach((to, from, next)=> {
if (to.name == 'Login') {
next();
return;
}
var name = store.state.user.name;
if (name == '未登录') {
if (to.meta.requireAuth || to.name == null) {
next({path: '/', query: {redirect: to.path}})
} else {
next();
}
} else {
initMenu(router, store);
next();
}
}
)
这里的代码很短,我来做一个简单的解释:
- 如果要去的页面是登录页面,这个没啥好说的,直接过。
- 如果不是登录页面的话,我先从 store 中获取当前的登录状态,如果未登录,则通过路由中 meta 属性的 requireAuth 属性判断要去的页面是否需要登录,如果需要登录,则跳回登录页面,同时将要去的页面的 path 作为参数传给登录页面,以便在登录成功之后跳转到目标页面,如果不需要登录,则直接过(事实上,本项目中只有 Login 页面不需要登录);如果已经登录了,则先初始化菜单,再跳转。
初始化菜单的操作如下:
export const initMenu = (router, store)=> {
if (store.state.routes.length > 0) {
return;
}
getRequest("/config/sysmenu").then(resp=> {
if (resp && resp.status == 200) {
var fmtRoutes = formatRoutes(resp.data);
router.addRoutes(fmtRoutes);
store.commit('initMenu', fmtRoutes);
}
})
}
export const formatRoutes = (routes)=> {
let fmRoutes = [];
routes.forEach(router=> {
let {
path,
component,
name,
meta,
iconCls,
children
} = router;
if (children && children instanceof Array) {
children = formatRoutes(children);
}
let fmRouter = {
path: path,
component(resolve){
if (component.startsWith("Home")) {
require(['../components/' + component + '.vue'], resolve)
} else if (component.startsWith("Emp")) {
require(['../components/emp/' + component + '.vue'], resolve)
} else if (component.startsWith("Per")) {
require(['../components/personnel/' + component + '.vue'], resolve)
} else if (component.startsWith("Sal")) {
require(['../components/salary/' + component + '.vue'], resolve)
} else if (component.startsWith("Sta")) {
require(['../components/statistics/' + component + '.vue'], resolve)
} else if (component.startsWith("Sys")) {
require(['../components/system/' + component + '.vue'], resolve)
}
},
name: name,
iconCls: iconCls,
meta: meta,
children: children
};
fmRoutes.push(fmRouter);
})
return fmRoutes;
}
在初始化菜单中,首先判断 store 中的数据是否存在,如果存在,说明这次跳转是正常的跳转,而不是用户按 F5 或者直接在地址栏输入某个地址进入的。否则就去加载菜单。拿到菜单之后,首先通过 formatRoutes 方法将服务器返回的 json 转为 router 需要的格式,这里主要是转 component,因为服务端返回的 component 是一个字符串,而 router 中需要的却是一个组件,因此我们在 formatRoutes 方法中动态的加载需要的组件即可。
数据格式准备成功之后,一方面将数据存到 store 中,另一方面利用路由中的 addRoutes 方法将之动态添加到路由中。
9.4 菜单渲染
最后,在 Home 页中,从 store 中获取菜单 json,渲染成菜单即可,相关代码可以在 Home.vue
中查看,不赘述。
扫码关注微信公众号 江南一点雨,回复 2TB,获取超 2TB Java 学习教程~