vue-router+vuex實現加載動態路由和菜單

前言

動態路由加載和動態菜單渲染的應用在後端權限控制中十分常見,後端只要加載權限路由進行渲染返回到瀏覽器就能夠。在先後端分離中,權限控制動態路由和動態菜單也是一個很是常見的問題。其實咱們最最理想的效果是什麼呢?
咱們訪問一個應用,在登陸以前有哪些路由是必定要加載的呢?你看我總結以下,你看下是否是這些:css

1.登陸路由 (登陸功能路由)
2.系統路由(系統消息路由,好比歡迎界面,404,error等的路由)

可是在vue中,一旦實例化,就必須初始化路由,但這個時候你尚未登陸,沒有獲取你的權限路由呀,若是加載所有路由,那麼在瀏覽器上輸入路由你就能夠訪問(這個問題可使用router.beforeEach鉤子進行權限鑑定解決),那麼在先後端分離的開發項目中,vue是如何實現動態路由加載實現權限控制的呢?這就是咱們這篇文章要寫的內容。前端

咱們寫事後臺渲染都知道怎麼去實現,那麼放到vue中如何去實現呢?咱們先羅列幾個問題進行思考,以下vue

1.vue中路由是如何初始化,放入到vue實例中的?
2.vue中提供了什麼實現動態路由加載呢?

咱們先順着這兩個問題進行思考,而且順着這兩個問題,咱們進行對應方案解決,這個過程當中會會出現不少新的問題,咱們也針對新問題出對應方案,而且進行優化。vue-router

路由初始化

路由初始化發生在何時呢?咱們能夠看主入口文件main.js,下面是我貼出的個人一個項目案例:vuex

import Vue from 'vue'

import 'normalize.css/normalize.css' // A modern alternative to CSS resets

import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

import '@/styles/index.scss' // global css

import App from './App'
import router from './router'
import store from './store'

import i18n from './lang' // Internationalization
import './icons' // icon
import './errorLog' // error log
import './permission' // permission control
import './mock' // simulation data

import * as filters from './filters' // global filters

Vue.use(Element, {
  size: 'medium', // set element-ui default size
  i18n: (key, value) => i18n.t(key, value)
})

// register global utility filters.
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key])
})

Vue.config.productionTip = false

// vue實例化就已經把router初始化了
new Vue({
  el: '#app',
  router,
  store,
  i18n,
  render: h => h(App)
})

經過上面的主入口文件,咱們就知道,這個路由初始胡就發生在vue實例化時。這個也很好理解若是你沒有初始化路由,那麼你就默認只能進入到主窗口,那麼接下來主窗口中你沒有路由你怎麼跳轉?程序也不知道你有哪些地方能夠跳轉呀,路由都是須要先註冊到實例中,實例才能定位到相應的視圖。從中咱們知道,路由初始化發生在vue實例化時element-ui

那麼這個時候咱們接着咱們想要的權限控制目標走:程序一開始,只註冊登陸路由、系統信息路由(歡迎頁面,404路由,error路由),咱們稱這些爲靜態路由,登陸後咱們經過接口獲取權限拿到了菜單,這個時候須要進行添加動態路由,把這些菜單信息註冊爲路由,咱們稱這些爲動態路由。那麼vue實例化時,vue-router就已經被初始化,那麼咱們是否是可以經過相似於往router實例裏面添加路由項的方式進行註冊路由呢?咱們能夠查閱文檔,也能夠查看vue-router源碼,有一個叫作addRoutes的方法進行動態註冊路由信息,路由對象其實就是一個路由數組,咱們經過addRoutes就能夠進行動態註冊路由,這個跟那個數組中extend功能相似的。後端

因此說道這裏咱們知道能夠經過addRoutes進行動態路由註冊。好,那麼咱們就順着這個思路走下去。api

在登陸模塊中,登陸成功後,咱們經過api獲取後臺權限菜單,而後註冊路由。代碼以下:數組

// 登陸頁登陸方法
handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid && this.isSuccess) {
          this.loading = true
          this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
           // 在這個時候進行獲取後臺權限及菜單
            this.$store.dispatch('getMenus', this.loginForm.name).then((res) => {
             // 把這個菜單信息註冊爲路由信息
              this.$router.addRoutes(menuitems)
            })
            this.loading = false
           // 除了登陸路由、和系統消息路由,這個跟路由是一個歡迎路由,是靜態路由
            this.$router.push({ path: '/' })
          }).catch(() => {
            this.$message.error('登錄失敗,請檢查用戶名或密碼是否正確')
            this.loading = false
          })
        } else {
          if (!this.isSuccess) {
            this.$message.error('請拉滑動條')
          }
          console.log('error submit!!')
          return false
        }
      })
  }

// 登陸方法計算屬性
computed: { 
   ...mapGetters([ 
    'menuitems', 
   ]) 
  },

總結一下:
登陸成功之後(持久化token),調用獲取權限菜單(保存在store裏面),這個時候就完成了登陸後動態初始化權限菜單的功能。那麼這裏面全部的路由就是當前用戶可訪問的菜單,就實現了咱們的目標效果。可是呢,store存儲權限菜單會有個問題,一旦刷新裏面的值就刷掉了,那麼這個時候就從新實例化的時候就會跳到404路由中,菜單信息也沒有了,那如何解決這個刷新時的問題呢?瀏覽器

咱們先分析一下思路:

1.初始化vue實例時,初始化router,包括全部的靜態路由。
2.全局鉤子檢查token是否有效?
        a.若是有效,則經過token獲取用戶信息保存到store中,根據用戶信息獲取權限菜單保存到store中,
        動態註冊權限菜單的路由信息;
        b.若是token無效,從新定位到靜態登陸路由進行登陸.
3.登陸模塊中,登陸成功後獲取用戶信息保存到store中,將token保存到store中並持久化到本地,
獲取權限菜單保存到store中,動態註冊權限菜單的路由信息
4.動態加載完路由後,直接跳到歡迎界面的靜態路由
5.一旦頁面刷新,那麼token就會從store中清除,token失效,那麼就會去得到持久化在本地的token
,從新去獲取用戶信息,權限菜單,從新動態註冊路由。
6.token持久化在本地也是有時間限制的,假設token有效期爲一週,一旦過了有效期,那麼會走2的b狀況。

那麼上面的思路就是動態加載權限菜單路由信息的簡述,整個的環路就通了,刷新問題就解決了。

代碼以下:

import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie

NProgress.configure({ showSpinner: false })// NProgress Configuration

// 權限判斷
function hasPermission(roles, permissionRoles) {
  if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
  if (!permissionRoles) return true
  return roles.some(role => permissionRoles.indexOf(role) >= 0)
}

const whiteList = ['/login', '/authredirect']// no redirect whitelist

// 全局鉤子
router.beforeEach((to, from, next) => {
  NProgress.start() // start progress bar
  // 若是有token
  if (getToken()) { // determine if there has token
    // 登陸後進入登陸頁
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
    } else {
      // 當進入非登陸頁時,須要進行權限校驗
      if (store.getters.roles.length === 0) { // 判斷當前用戶是否已拉取完user_info信息
        store.dispatch('GetUserInfo').then(res => { // 拉取user_info
           const roles = res.data.data.roles // note: roles must be a array! such as: ['editor','develop']
           store.dispatch('GenerateRoutes', { roles }).then(() => { // 根據roles權限生成可訪問的路由表
             router.addRoutes(store.getters.addRouters) // 動態添加可訪問路由表
             next({ ...to, replace: true }) // hack方法 確保addRoutes已完成 ,set the replace: 
           })
        }).catch((err) => {
          store.dispatch('FedLogOut').then(() => {
            Message.error(err || 'Verification failed, please login again')
            next({ path: '/' })
          })
        })
      } else {
        // 沒有動態改變權限的需求可直接next() 刪除下方權限判斷 ↓
        if (hasPermission(store.getters.roles, to.meta.roles)) {
          next()
        } else {
          next({ path: '/401', replace: true, query: { noGoBack: true }})
        }
        // 可刪 ↑
      }
    }
  } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) { // 在免登陸白名單,直接進入
      next()
    } else {
      next('/login') // 不然所有重定向到登陸頁
      NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
    }
  }
})

router.afterEach(() => {
  NProgress.done() // finish progress bar
})

備註:根據模塊獨立性,我把登陸中獲取權限列表去掉,都放置在全局鉤子中,把上面的代碼直接引入到主入口文件main.js中。

另外這裏採用vuex進行狀態管理,因此重新捋一下思路:

1.vue實例化,初始化靜態路由
2.全局鉤子進行檢查:
    a.token有效
          -若是當前跳轉路由是登陸路由,直接進入根路由/
            -若是跳轉路由非登陸路由,則須要進行權限校驗,若是用戶信息和權限菜單沒拉取,
            則進行拉取後將權限菜單動態註冊到router中,進行權限判斷,若是有用戶信息和權限菜單信息,
            則直接進行權限判斷。
    b.token無效
          -若是在白名單中,則直接進入
            -進入到登陸頁

3.全局狀態管理採用vuex

到這裏咱們就已經完成了vue-router+vuex動態註冊路由控制權限的方式就說完了,這裏我留個思考題給你們:如今根據上面的方式我再引入一個產品實體,(用戶 - 產品 - 菜單 ), 用戶能夠有多個產品權限,每一個產品有公用的菜單,也有各產品定製化的菜單,那麼這個時候我在前端若是作好權限校驗呢?要求:當前用戶當前產品的權限菜單纔可被訪問。

相關文章
相關標籤/搜索