您當前位置: 南順網絡>> 官方資訊>> 建站知識

權限管理(lǐ)模塊中動态加載Vue組件

當前後端分離(lí)時,權限問題的(de)處理(lǐ)也和(hé)我們傳統的(de)處理(lǐ)方式有一(yī)點差異。筆(bǐ)者前幾天剛好在負責一(yī)個項目的(de)權限管理(lǐ)模塊,現在權限管理(lǐ)模塊已經做(zuò)完了,我想通過5-6篇文章(zhāng),來介紹一(yī)下項目中遇到的(de)問題以及我的(de)解決方案,希望這個系列能夠給小夥伴一(yī)些幫助。本系列文章(zhāng)并不是手把手的(de)教程,主要介紹了核心思路并講解了核心代碼,完整的(de)代碼小夥伴們可(kě)以在GitHub上star并clone下來研究。另外,原本計劃把項目跑起來放到網上供小夥伴們查看,但是之前買服務器為(wèi)了省錢,內(nèi)存隻有512M,兩個應用跑不起來(已經有一(yī)個V部落開源項目在運行),因此小夥伴們隻能将就看一(yī)下下面的(de)截圖了,GitHub上有部署教程,部署到本地(dì)也可(kě)以查看完整效果。


項目地(dì)址:https://github.com/lenve/vhr

前面幾篇文章(zhāng),我們已經基本解決了服務端的(de)問題,并封裝了前端請求,本文我們主要來聊聊登錄以及組件的(de)動态加載。

本文是本系列的(de)第五篇,建議先閱讀前面的(de)文章(zhāng)有助于更好的(de)理(lǐ)解本文:

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

登錄狀态保存

當用戶登錄成功之後,需要将當前用戶的(de)登錄信息保存在本地(dì),方便後面使用。具體實現如(rú)下:

登錄成功保存數據

在登錄操作執行成功之後,通過commit操作将數據提交到store中,核心代碼如(rú)下:

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的(de)核心代碼如(rú)下:

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');
    }
  }});

為(wèi)了減少麻煩,用戶登錄成功後的(de)數據将被保存在localStorage中(防止用戶按F5刷新之後數據丢失),以字符串的(de)形式存入,取的(de)時候再轉為(wèi)json。當用戶注銷登陸時,将localStorage中的(de)數據清除。

組件動态加載

在權限管理(lǐ)模塊中,這算是前端的(de)核心了。

核心思路

用戶在登錄成功之後,進入home主頁之前,向服務端發送請求,要求獲取當前的(de)菜單信息和(hé)組件信息,服務端根據當前用戶所具備的(de)角色,以及角色所對應的(de)資源,返回一(yī)個json字符串,格式如(rú)下:

[
    {
        "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": "高(gāo)級資料",
                "iconCls": null,
                "children": [],
                "meta": {
                    "keepAlive": false,
                    "requireAuth": true
                }
            }
        ],
        "meta": {
            "keepAlive": false,
            "requireAuth": true
        }
    }]

前端在拿到這個字符串之後,做(zuò)兩件事:1.将json動态添加到當前路由中;2.将數據保存到store中,然後各頁面根據store中的(de)數據來渲染菜單。

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

數據請求時機

這個很重要。

可(kě)能會有小夥伴說這有何難,登錄成功之後請求不就可(kě)以了嗎?是的(de),登錄成功之後,請求菜單資源是可(kě)以的(de),請求到之後,我們将之保存在store中,以便下一(yī)次使用,但是這樣又會有另外一(yī)個問題,假如(rú)用戶登錄成功之後,點擊某一(yī)個子(zǐ)頁面,進入到子(zǐ)頁面中,然後按了一(yī)下F5進行刷新,這個時候就GG了,因為(wèi)F5刷新之後store中的(de)數據就沒了,而我們又隻在登錄成功的(de)時候請求了一(yī)次菜單資源,要解決這個問題,有兩種思路:1.将菜單資源不要保存到store中,而是保存到localStorage中,這樣即使F5刷新之後數據還在;2.直接在每一(yī)個頁面的(de)mounted方法中,都去(qù)加載一(yī)次菜單資源。

由于菜單資源是非常敏感的(de),因此最好不要不要将其保存到本地(dì),故舍棄方案1,但是方案2的(de)工作量有點大,因此我采取辦法将之簡化,采取的(de)辦法就是使用路由中的(de)導航守衛。

路由導航守衛

我的(de)具體實現是這樣的(de),首先在store中創建一(yī)個routes數組,這是一(yī)個空數組,然後開啓路由全局守衛,如(rú)下:

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();
    }
  })

這裏的(de)代碼很短(duǎn),我來做(zuò)一(yī)個簡單的(de)解釋:
1.如(rú)果要去(qù)的(de)頁面是登錄頁面,這個沒啥好說的(de),直接過。

2.如(rú)果不是登錄頁面的(de)話,我先從store中獲取當前的(de)登錄狀态,如(rú)果未登錄,則通過路由中meta屬性的(de)requireAuth屬性判斷要去(qù)的(de)頁面是否需要登錄,如(rú)果需要登錄,則跳回登錄頁面,同時将要去(qù)的(de)頁面的(de)path作為(wèi)參數傳給登錄頁面,以便在登錄成功之後跳轉到目标頁面,如(rú)果不需要登錄,則直接過(事實上,本項目中隻有Login頁面不需要登錄);如(rú)果已經登錄了,則先初始化菜單,再跳轉。

初始化菜單的(de)操作如(rú)下:

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中的(de)數據是否存在,如(rú)果存在,說明這次跳轉是正常的(de)跳轉,而不是用戶按F5或者直接在地(dì)址欄輸入某個地(dì)址進入的(de)。否則就去(qù)加載菜單。拿到菜單之後,首先通過formatRoutes方法将服務器返回的(de)json轉為(wèi)router需要的(de)格式,這裏主要是轉component,因為(wèi)服務端返回的(de)component是一(yī)個字符串,而router中需要的(de)卻是一(yī)個組件,因此我們在formatRoutes方法中動态的(de)加載需要的(de)組件即可(kě)。數據格式準備成功之後,一(yī)方面将數據存到store中,另一(yī)方面利用路由中的(de)addRoutes方法将之動态添加到路由中。

菜單渲染

最後,在Home頁中,從store中獲取菜單json,渲染成菜單即可(kě),相關代碼可(kě)以在Home.vue中查看,不贅述。

OK,如(rú)此之後,不同用戶登錄成功之後就可(kě)以看到不同的(de)菜單了。


編輯:--ns868