ets 使用 初始化 vuex patch indexof 狀態管理 效果 trigger

前言

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

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

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

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

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

我們先順著這兩個問題進行思考,並且順著這兩個問題,我們進行對應方案解決,這個過程中會會出現很多新的問題,我們也針對新問題出對應方案,並且進行優化。

路由初始化

路由初始化發生在什麽時候呢?我們可以看主入口文件main.js,下面是我貼出的我的一個項目案例:

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實例化時

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

所以說道這裏我們知道可以通過addRoutes進行動態路由註冊。好,那麽我們就順著這個思路走下去。

在登錄模塊中,登錄成功後,我們通過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動態註冊路由控制權限的方式就說完了,這裏我留個思考題給大家:現在根據上面的方式我再引入一個產品實體,(用戶 - 產品 - 菜單 ), 用戶可以有多個產品權限,每個產品有公用的菜單,也有各產品定制化的菜單,那麽這個時候我在前端如果做好權限校驗呢?要求:當前用戶當前產品的權限菜單才可被訪問。

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