動態路由加載和動態菜單渲染的應用在後端權限控制中十分常見,後端只要加載權限路由進行渲染返回到瀏覽器就能夠。在先後端分離中,權限控制動態路由和動態菜單也是一個很是常見的問題。其實咱們最最理想的效果是什麼呢?
咱們訪問一個應用,在登陸以前有哪些路由是必定要加載的呢?你看我總結以下,你看下是否是這些: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動態註冊路由控制權限的方式就說完了,這裏我留個思考題給你們:如今根據上面的方式我再引入一個產品實體,(用戶 - 產品 - 菜單 ), 用戶能夠有多個產品權限,每一個產品有公用的菜單,也有各產品定製化的菜單,那麼這個時候我在前端若是作好權限校驗呢?要求:當前用戶當前產品的權限菜單纔可被訪問。