天天看點

權限管理子產品中動态加載Vue元件登入狀态儲存元件動态加載

目前後端分離時,權限問題的處理也和我們傳統的處理方式有一點差異。筆者前幾天剛好在負責一個項目的權限管理子產品,現在權限管理子產品已經做完了,我想通過5-6篇文章,來介紹一下項目中遇到的問題以及我的解決方案,希望這個系列能夠給小夥伴一些幫助。本系列文章并不是手把手的教程,主要介紹了核心思路并講解了核心代碼,完整的代碼小夥伴們可以在GitHub上star并clone下來研究。另外,原本計劃把項目跑起來放到網上供小夥伴們檢視,但是之前買伺服器為了省錢,記憶體隻有512M,兩個應用跑不起來(已經有一個V部落開源項目在運作),是以小夥伴們隻能将就看一下下面的截圖了,GitHub上有部署教程,部署到本地也可以檢視完整效果。

項目位址:https://github.com/lenve/vhr

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

登入狀态儲存

當使用者登入成功之後,需要将目前使用者的登入資訊儲存在本地,友善後面使用。具體實作如下:

登入成功儲存資料

在登入操作執行成功之後,通過commit操作将資料送出到store中,核心代碼如下:

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,如此之後,不同使用者登入成功之後就可以看到不同的菜單了。