權限管理模塊中動態加載Vue組件

當先後端分離時,權限問題的處理也和咱們傳統的處理方式有一點差別。筆者前幾天恰好在負責一個項目的權限管理模塊,如今權限管理模塊已經作完了,我想經過5-6篇文章,來介紹一下項目中遇到的問題以及個人解決方案,但願這個系列可以給小夥伴一些幫助。本系列文章並非手把手的教程,主要介紹了核心思路並講解了核心代碼,完整的代碼小夥伴們能夠在GitHub上star並clone下來研究。另外,本來計劃把項目跑起來放到網上供小夥伴們查看,可是以前買服務器爲了省錢,內存只有512M,兩個應用跑不起來(已經有一個V部落開源項目在運行),所以小夥伴們只能將就看一下下面的截圖了,GitHub上有部署教程,部署到本地也能夠查看完整效果。前端


項目地址:https://github.com/lenve/vhr vue

前面幾篇文章,咱們已經基本解決了服務端的問題,並封裝了前端請求,本文咱們主要來聊聊登陸以及組件的動態加載。 ios

本文是本系列的第五篇,建議先閱讀前面的文章有助於更好的理解本文: git

1.SpringBoot+Vue先後端分離,使用SpringSecurity完美處理權限問題(一)
2.SpringBoot+Vue先後端分離,使用SpringSecurity完美處理權限問題(二)
3.SpringSecurity中密碼加鹽與SpringBoot中異常統一處理
4.axios請求封裝和異常統一處理github

登陸狀態保存

當用戶登陸成功以後,須要將當前用戶的登陸信息保存在本地,方便後面使用。具體實現以下:json

登陸成功保存數據

在登陸操做執行成功以後,經過commit操做將數據提交到store中,核心代碼以下:axios

this.postRequest('/login', {
    username: this.loginForm.username,
    password: this.loginForm.password
}).then(resp=> {
    if (resp && resp.status == 200) {
    var data = resp.data;
    _this.$store.commit('login', data.msg);
    var path = _this.$route.query.redirect;
    _this.$router.replace({path: path == '/' || path == undefined ? '/home' : path});
    }
});

store

store的核心代碼以下:後端

export default new Vuex.Store({
  state: {
    user: {
      name: window.localStorage.getItem('user' || '[]') == null ? '未登陸' : JSON.parse(window.localStorage.getItem('user' || '[]')).name,
      userface: window.localStorage.getItem('user' || '[]') == null ? '' : JSON.parse(window.localStorage.getItem('user' || '[]')).userface
    }
  },
  mutations: {
    login(state, user){
      state.user = user;
      window.localStorage.setItem('user', JSON.stringify(user));
    },
    logout(state){
      window.localStorage.removeItem('user');
    }
  }
});

爲了減小麻煩,用戶登陸成功後的數據將被保存在localStorage中(防止用戶按F5刷新以後數據丟失),以字符串的形式存入,取的時候再轉爲json。當用戶註銷登錄時,將localStorage中的數據清除。數組

組件動態加載

在權限管理模塊中,這算是前端的核心了。服務器

核心思路

用戶在登陸成功以後,進入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
        }
    }
]

前端在拿到這個字符串以後,作兩件事:1.將json動態添加到當前路由中;2.將數據保存到store中,而後各頁面根據store中的數據來渲染菜單。

核心思路並不難,下面咱們來看看實現步驟。

數據請求時機

這個很重要。

可能會有小夥伴說這有何難,登陸成功以後請求不就能夠了嗎?是的,登陸成功以後,請求菜單資源是能夠的,請求到以後,咱們將之保存在store中,以便下一次使用,可是這樣又會有另一個問題,假如用戶登陸成功以後,點擊某一個子頁面,進入到子頁面中,而後按了一下F5進行刷新,這個時候就GG了,由於F5刷新以後store中的數據就沒了,而咱們又只在登陸成功的時候請求了一次菜單資源,要解決這個問題,有兩種思路:1.將菜單資源不要保存到store中,而是保存到localStorage中,這樣即便F5刷新以後數據還在;2.直接在每個頁面的mounted方法中,都去加載一次菜單資源。

因爲菜單資源是很是敏感的,所以最好不要不要將其保存到本地,故舍棄方案1,可是方案2的工做量有點大,所以我採起辦法將之簡化,採起的辦法就是使用路由中的導航守衛。

路由導航守衛

個人具體實現是這樣的,首先在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();
    }
  }
)

這裏的代碼很短,我來作一個簡單的解釋:
1.若是要去的頁面是登陸頁面,這個沒啥好說的,直接過。

2.若是不是登陸頁面的話,我先從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方法將之動態添加到路由中。

菜單渲染

最後,在Home頁中,從store中獲取菜單json,渲染成菜單便可,相關代碼能夠在Home.vue中查看,不贅述。

OK,如此以後,不一樣用戶登陸成功以後就能夠看到不一樣的菜單了。

關注公衆號,能夠及時接收到最新文章:

圖片描述

相關文章
相關標籤/搜索