在权限管理模块中,这算是前端的核心了。

核心思路

用户在登录成功之后,进入home主页之前,向服务端发送请求,要求获取当前的菜单信息和组件信息,服务端根据当前用户所具备的角色,以及角色所对应的资源,返回一个json字符串,格式如下:

  1. [
  2. {
  3. "id": 2,
  4. "path": "/home",
  5. "component": "Home",
  6. "name": "员工资料",
  7. "iconCls": "fa fa-user-circle-o",
  8. "children": [
  9. {
  10. "id": null,
  11. "path": "/emp/basic",
  12. "component": "EmpBasic",
  13. "name": "基本资料",
  14. "iconCls": null,
  15. "children": [],
  16. "meta": {
  17. "keepAlive": false,
  18. "requireAuth": true
  19. }
  20. },
  21. {
  22. "id": null,
  23. "path": "/emp/adv",
  24. "component": "EmpAdv",
  25. "name": "高级资料",
  26. "iconCls": null,
  27. "children": [],
  28. "meta": {
  29. "keepAlive": false,
  30. "requireAuth": true
  31. }
  32. }
  33. ],
  34. "meta": {
  35. "keepAlive": false,
  36. "requireAuth": true
  37. }
  38. }
  39. ]

前端在拿到这个字符串之后,做两件事:1.将json动态添加到当前路由中;2.将数据保存到store中,然后各页面根据store中的数据来渲染菜单。

核心思路并不难,下面我们来看看实现步骤。

数据请求时机

这个很重要。

可能会有小伙伴说这有何难,登录成功之后请求不就可以了吗?是的,登录成功之后,请求菜单资源是可以的,请求到之后,我们将之保存在store中,以便下一次使用,但是这样又会有另外一个问题,假如用户登录成功之后,点击某一个子页面,进入到子页面中,然后按了一下F5进行刷新,这个时候就GG了,因为F5刷新之后store中的数据就没了,而我们又只在登录成功的时候请求了一次菜单资源,要解决这个问题,有两种思路:1.将菜单资源不要保存到store中,而是保存到localStorage中,这样即使F5刷新之后数据还在;2.直接在每一个页面的mounted方法中,都去加载一次菜单资源。

由于菜单资源是非常敏感的,因此最好不要不要将其保存到本地,故舍弃方案1,但是方案2的工作量有点大,因此我采取办法将之简化,采取的办法就是使用路由中的导航守卫。

路由导航守卫

我的具体实现是这样的,首先在store中创建一个routes数组,这是一个空数组,然后开启路由全局守卫,如下:

  1. router.beforeEach((to, from, next)=> {
  2. if (to.name == 'Login') {
  3. next();
  4. return;
  5. }
  6. var name = store.state.user.name;
  7. if (name == '未登录') {
  8. if (to.meta.requireAuth || to.name == null) {
  9. next({path: '/', query: {redirect: to.path}})
  10. } else {
  11. next();
  12. }
  13. } else {
  14. initMenu(router, store);
  15. next();
  16. }
  17. }
  18. )

这里的代码很短,我来做一个简单的解释:1.如果要去的页面是登录页面,这个没啥好说的,直接过。

2.如果不是登录页面的话,我先从store中获取当前的登录状态,如果未登录,则通过路由中meta属性的requireAuth属性判断要去的页面是否需要登录,如果需要登录,则跳回登录页面,同时将要去的页面的path作为参数传给登录页面,以便在登录成功之后跳转到目标页面,如果不需要登录,则直接过(事实上,本项目中只有Login页面不需要登录);如果已经登录了,则先初始化菜单,再跳转。

初始化菜单的操作如下:

  1. export const initMenu = (router, store)=> {
  2. if (store.state.routes.length > 0) {
  3. return;
  4. }
  5. getRequest("/config/sysmenu").then(resp=> {
  6. if (resp && resp.status == 200) {
  7. var fmtRoutes = formatRoutes(resp.data);
  8. router.addRoutes(fmtRoutes);
  9. store.commit('initMenu', fmtRoutes);
  10. }
  11. })
  12. }
  13. export const formatRoutes = (routes)=> {
  14. let fmRoutes = [];
  15. routes.forEach(router=> {
  16. let {
  17. path,
  18. component,
  19. name,
  20. meta,
  21. iconCls,
  22. children
  23. } = router;
  24. if (children && children instanceof Array) {
  25. children = formatRoutes(children);
  26. }
  27. let fmRouter = {
  28. path: path,
  29. component(resolve){
  30. if (component.startsWith("Home")) {
  31. require(['../components/' + component + '.vue'], resolve)
  32. } else if (component.startsWith("Emp")) {
  33. require(['../components/emp/' + component + '.vue'], resolve)
  34. } else if (component.startsWith("Per")) {
  35. require(['../components/personnel/' + component + '.vue'], resolve)
  36. } else if (component.startsWith("Sal")) {
  37. require(['../components/salary/' + component + '.vue'], resolve)
  38. } else if (component.startsWith("Sta")) {
  39. require(['../components/statistics/' + component + '.vue'], resolve)
  40. } else if (component.startsWith("Sys")) {
  41. require(['../components/system/' + component + '.vue'], resolve)
  42. }
  43. },
  44. name: name,
  45. iconCls: iconCls,
  46. meta: meta,
  47. children: children
  48. };
  49. fmRoutes.push(fmRouter);
  50. })
  51. return fmRoutes;
  52. }

在初始化菜单中,首先判断store中的数据是否存在,如果存在,说明这次跳转是正常的跳转,而不是用户按F5或者直接在地址栏输入某个地址进入的。否则就去加载菜单。拿到菜单之后,首先通过formatRoutes方法将服务器返回的json转为router需要的格式,这里主要是转component,因为服务端返回的component是一个字符串,而router中需要的却是一个组件,因此我们在formatRoutes方法中动态的加载需要的组件即可。数据格式准备成功之后,一方面将数据存到store中,另一方面利用路由中的addRoutes方法将之动态添加到路由中。

菜单渲染

最后,在Home页中,从store中获取菜单json,渲染成菜单即可,相关代码可以在Home.vue中查看,不赘述。