動(dòng)態(tài)路由加載和動(dòng)態(tài)菜單渲染的應(yīng)用在后端權(quán)限控制中十分常見,后端只要加載權(quán)限路由進(jìn)行渲染返回到瀏覽器就可以。在前后端分離中,權(quán)限控制動(dòng)態(tài)路由和動(dòng)態(tài)菜單也是一個(gè)非常常見的問題。其實(shí)我們最最理想的效果是什么呢?
我們?cè)L問一個(gè)應(yīng)用,在登錄之前有哪些路由是一定要加載的呢?你看我總結(jié)如下,你看下是不是這些:
創(chuàng)新互聯(lián)專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于網(wǎng)站設(shè)計(jì)制作、成都網(wǎng)站設(shè)計(jì)、鳳慶網(wǎng)絡(luò)推廣、重慶小程序開發(fā)、鳳慶網(wǎng)絡(luò)營銷、鳳慶企業(yè)策劃、鳳慶品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運(yùn)營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們最大的嘉獎(jiǎng);創(chuàng)新互聯(lián)為所有大學(xué)生創(chuàng)業(yè)者提供鳳慶建站搭建服務(wù),24小時(shí)服務(wù)熱線:18980820575,官方網(wǎng)址:www.cdcxhl.com
1.登錄路由 (登錄功能路由)
2.系統(tǒng)路由(系統(tǒng)消息路由,比如歡迎界面,404,error等的路由)
但是在vue中,一旦實(shí)例化,就必須初始化路由,但這個(gè)時(shí)候你還沒有登錄,沒有獲取你的權(quán)限路由呀,如果加載全部路由,那么在瀏覽器上輸入路由你就可以訪問(這個(gè)問題可以使用router.beforeEach鉤子進(jìn)行權(quán)限鑒定解決),那么在前后端分離的開發(fā)項(xiàng)目中,vue是如何實(shí)現(xiàn)動(dòng)態(tài)路由加載實(shí)現(xiàn)權(quán)限控制的呢?這就是我們這篇文章要寫的內(nèi)容。
我們寫過后臺(tái)渲染都知道怎么去實(shí)現(xiàn),那么放到vue中如何去實(shí)現(xiàn)呢?我們先羅列幾個(gè)問題進(jìn)行思考,如下
1.vue中路由是如何初始化,放入到vue實(shí)例中的?
2.vue中提供了什么實(shí)現(xiàn)動(dòng)態(tài)路由加載呢?
我們先順著這兩個(gè)問題進(jìn)行思考,并且順著這兩個(gè)問題,我們進(jìn)行對(duì)應(yīng)方案解決,這個(gè)過程中會(huì)會(huì)出現(xiàn)很多新的問題,我們也針對(duì)新問題出對(duì)應(yīng)方案,并且進(jìn)行優(yōu)化。
路由初始化發(fā)生在什么時(shí)候呢?我們可以看主入口文件main.js,下面是我貼出的我的一個(gè)項(xiàng)目案例:
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實(shí)例化就已經(jīng)把router初始化了
new Vue({
el: '#app',
router,
store,
i18n,
render: h => h(App)
})
通過上面的主入口文件,我們就知道,這個(gè)路由初始胡就發(fā)生在vue實(shí)例化時(shí)。這個(gè)也很好理解如果你沒有初始化路由,那么你就默認(rèn)只能進(jìn)入到主窗口,那么接下來主窗口中你沒有路由你怎么跳轉(zhuǎn)?程序也不知道你有哪些地方可以跳轉(zhuǎn)呀,路由都是需要先注冊(cè)到實(shí)例中,實(shí)例才能定位到相應(yīng)的視圖。從中我們知道,路由初始化發(fā)生在vue實(shí)例化時(shí)。
那么這個(gè)時(shí)候我們接著我們想要的權(quán)限控制目標(biāo)走:程序一開始,只注冊(cè)登錄路由、系統(tǒng)信息路由(歡迎頁面,404路由,error路由),我們稱這些為靜態(tài)路由,登錄后我們通過接口獲取權(quán)限拿到了菜單,這個(gè)時(shí)候需要進(jìn)行添加動(dòng)態(tài)路由,把這些菜單信息注冊(cè)為路由,我們稱這些為動(dòng)態(tài)路由。那么vue實(shí)例化時(shí),vue-router就已經(jīng)被初始化,那么我們是不是能夠通過類似于往router實(shí)例里面添加路由項(xiàng)的方式進(jìn)行注冊(cè)路由呢?我們可以查閱文檔,也可以查看vue-router源碼,有一個(gè)叫做addRoutes的方法進(jìn)行動(dòng)態(tài)注冊(cè)路由信息,路由對(duì)象其實(shí)就是一個(gè)路由數(shù)組,我們通過addRoutes就可以進(jìn)行動(dòng)態(tài)注冊(cè)路由,這個(gè)跟那個(gè)數(shù)組中extend功能類似的。
所以說道這里我們知道可以通過addRoutes進(jìn)行動(dòng)態(tài)路由注冊(cè)。好,那么我們就順著這個(gè)思路走下去。
在登錄模塊中,登錄成功后,我們通過api獲取后臺(tái)權(quán)限菜單,然后注冊(cè)路由。代碼如下:
// 登錄頁登錄方法
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid && this.isSuccess) {
this.loading = true
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
// 在這個(gè)時(shí)候進(jìn)行獲取后臺(tái)權(quán)限及菜單
this.$store.dispatch('getMenus', this.loginForm.name).then((res) => {
// 把這個(gè)菜單信息注冊(cè)為路由信息
this.$router.addRoutes(menuitems)
})
this.loading = false
// 除了登錄路由、和系統(tǒng)消息路由,這個(gè)跟路由是一個(gè)歡迎路由,是靜態(tài)路由
this.$router.push({ path: '/' })
}).catch(() => {
this.$message.error('登陸失敗,請(qǐng)檢查用戶名或密碼是否正確')
this.loading = false
})
} else {
if (!this.isSuccess) {
this.$message.error('請(qǐng)拉滑動(dòng)條')
}
console.log('error submit!!')
return false
}
})
}
// 登錄方法計(jì)算屬性
computed: {
...mapGetters([
'menuitems',
])
},
總結(jié)一下:
登錄成功以后(持久化token),調(diào)用獲取權(quán)限菜單(保存在store里面),這個(gè)時(shí)候就完成了登錄后動(dòng)態(tài)初始化權(quán)限菜單的功能。那么這里面所有的路由就是當(dāng)前用戶可訪問的菜單,就實(shí)現(xiàn)了我們的目標(biāo)效果。但是呢,store存儲(chǔ)權(quán)限菜單會(huì)有個(gè)問題,一旦刷新里面的值就刷掉了,那么這個(gè)時(shí)候就重新實(shí)例化的時(shí)候就會(huì)跳到404路由中,菜單信息也沒有了,那如何解決這個(gè)刷新時(shí)的問題呢?
我們先分析一下思路:
1.初始化vue實(shí)例時(shí),初始化router,包括所有的靜態(tài)路由。
2.全局鉤子檢查token是否有效?
a.如果有效,則通過token獲取用戶信息保存到store中,根據(jù)用戶信息獲取權(quán)限菜單保存到store中,
動(dòng)態(tài)注冊(cè)權(quán)限菜單的路由信息;
b.如果token無效,重新定位到靜態(tài)登錄路由進(jìn)行登錄.
3.登錄模塊中,登錄成功后獲取用戶信息保存到store中,將token保存到store中并持久化到本地,
獲取權(quán)限菜單保存到store中,動(dòng)態(tài)注冊(cè)權(quán)限菜單的路由信息
4.動(dòng)態(tài)加載完路由后,直接跳到歡迎界面的靜態(tài)路由
5.一旦頁面刷新,那么token就會(huì)從store中清除,token失效,那么就會(huì)去獲得持久化在本地的token
,重新去獲取用戶信息,權(quán)限菜單,重新動(dòng)態(tài)注冊(cè)路由。
6.token持久化在本地也是有時(shí)間限制的,假設(shè)token有效期為一周,一旦過了有效期,那么會(huì)走2的b情況。
那么上面的思路就是動(dòng)態(tài)加載權(quán)限菜單路由信息的簡述,整個(gè)的環(huán)路就通了,刷新問題就解決了。
代碼如下:
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
// 權(quán)限判斷
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
// 登錄后進(jìn)入登錄頁
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else {
// 當(dāng)進(jìn)入非登錄頁時(shí),需要進(jìn)行權(quán)限校驗(yàn)
if (store.getters.roles.length === 0) { // 判斷當(dāng)前用戶是否已拉取完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(() => { // 根據(jù)roles權(quán)限生成可訪問的路由表
router.addRoutes(store.getters.addRouters) // 動(dòng)態(tài)添加可訪問路由表
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 {
// 沒有動(dòng)態(tài)改變權(quán)限的需求可直接next() 刪除下方權(quán)限判斷 ↓
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) { // 在免登錄白名單,直接進(jìn)入
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
})
備注:根據(jù)模塊獨(dú)立性,我把登錄中獲取權(quán)限列表去掉,都放置在全局鉤子中,把上面的代碼直接引入到主入口文件main.js中。
另外這里采用vuex進(jìn)行狀態(tài)管理,所以從新捋一下思路:
1.vue實(shí)例化,初始化靜態(tài)路由
2.全局鉤子進(jìn)行檢查:
a.token有效
-如果當(dāng)前跳轉(zhuǎn)路由是登錄路由,直接進(jìn)入根路由/
-如果跳轉(zhuǎn)路由非登錄路由,則需要進(jìn)行權(quán)限校驗(yàn),如果用戶信息和權(quán)限菜單沒拉取,
則進(jìn)行拉取后將權(quán)限菜單動(dòng)態(tài)注冊(cè)到router中,進(jìn)行權(quán)限判斷,如果有用戶信息和權(quán)限菜單信息,
則直接進(jìn)行權(quán)限判斷。
b.token無效
-如果在白名單中,則直接進(jìn)入
-進(jìn)入到登錄頁
3.全局狀態(tài)管理采用vuex
到這里我們就已經(jīng)完成了vue-router+vuex動(dòng)態(tài)注冊(cè)路由控制權(quán)限的方式就說完了,這里我留個(gè)思考題給大家:現(xiàn)在根據(jù)上面的方式我再引入一個(gè)產(chǎn)品實(shí)體,(用戶 - 產(chǎn)品 - 菜單 ), 用戶可以有多個(gè)產(chǎn)品權(quán)限,每個(gè)產(chǎn)品有公用的菜單,也有各產(chǎn)品定制化的菜單,那么這個(gè)時(shí)候我在前端如果做好權(quán)限校驗(yàn)?zāi)??要求:?dāng)前用戶當(dāng)前產(chǎn)品的權(quán)限菜單才可被訪問。