作者:lxcan
轉發鏈接:https://segmentfault.com/a/1190000022431839
一、整體思路
後端返回用戶權限,前端根據用戶權限處理得到左側菜單;所有路由在前端定義好,根據後端返回的用戶權限篩選出需要掛載的路由,然後使用 addRoutes 動態掛載路由。
二、實現要點
(1)路由定義,分為初始路由和動態路由,一般來說初始路由只有 login,其他路由都掛載在 home 路由之下需要動態掛載。
(2)用戶登錄,登錄成功之後得到 token,保存在 sessionStorage,跳轉到 home,此時會進入路由攔截根據 token 獲取用戶權限列表。、
(3)全局路由攔截,根據當前用戶有沒有 token 和 權限列表進行相應的判斷和跳轉,當沒有 token 時跳到 login,當有 token 而沒有權限列表時去發請求獲取權限等等邏輯。
(4)處理用戶權限,在 store.js 定義一個模塊 permission.js,專門用於處理用戶權限相關的邏輯,用戶權限列表、菜單列表都保存在此模塊;
(5)用戶權限列表、菜單列表的處理,前端的路由要和後端返回的權限有一個唯一標識(一般用路由名做標識符),根據此標識篩選出對應的路由。
(6)左側菜單,要和用戶信息、用戶管理模塊使用的菜單信息一致,統一使用保存在 store 中的變量。
三、具體實現流程
1、準備工作,路由定義
<code>let
router =new
Router({mode
:'history'
,routes
: [ {path
:'/login'
,name
:'login'
,component
:()
=>import
('@/views/login.vue'
), }, ] });/<code>
<code>export
const
dynamicRoutes = [ {path
:'/'
,name
:'home'
,component
:()
=>import
('@/views/home.vue'
),meta
: {requiresAuth
:true
, },children
: [ {path
:'/user-info'
,name
:'user-info'
,component
:()
=>import
('@/views/user-setting/user-info.vue'
), }, {path
:'/user-password'
,name
:'user-password'
,component
:()
=>import
('@/views/user-setting/user-password.vue'
), }, ] }, {path
:'/403'
,component
:()
=>import
('@/views/error-page/403'
), }, {path
:'*'
,component
:()
=>import
('@/views/error-page/404'
), }, ];/<code>
系統主要頁面的路由,後續會將這些路由經過權限篩選,添加到 home 路由的 children 裡面
<code>export
default
[ {path
:'/deploy-manage'
,name
:'deploy-manage'
,component
:()
=>import
('@/views/sys-admin/deploy-manage/deploy-manage.vue'
),meta
: {permitName
:'deploy-manage'
, } }, ];/<code>
2、用戶登錄
用戶進入登錄頁,輸入用戶名、密碼、驗證碼,點擊登錄,發送登錄請求,登錄成功之後,將 token 保存在 sessionStorage,然後跳轉到首頁 /home ,進入路由攔截的邏輯。
<code> vm.$http.login(params,data
=> { sessionStorage.token = data.token; vm.$router.push({ name:'home'
}); },err
=> {console
.log(err); });/<code>
3、全局路由攔截
首先從打開本地服務 http://localhost:2001 開始,打開後會進入 login 頁面,那麼判斷的依據是什麼?首先是 token。沒有登錄的用戶是拿不到 token 的,而登錄後的用戶我們會將 token 存到 seesionStorage,因此,根據當前有沒有 token 即可知道是否登錄。
<code>router
.beforeEach
((to, from, next) => {if
(!sessionStorage.token) {if
(to.matched.length >0
&& !to.matched.some(item => item.meta.requiresAuth)) {next
(); }else
{next
({path
:'/login'
}); } } else {if
(!store.state.permission.permissionList) {store
.dispatch
('permission/FETCH_PERMISSION'
).then
(() => {next
({path
: to.path,query
: to.query }); }); }else
{if
(to.path !=='/login'
) {if
(to.matched.length ===0
) {next
({path
:'/403'
}); } else if (queryChange) {next
({name
: to.name,params
: to.params,query
: to.query }); } else if (sessionStorage.isSysLock ==='true'
&& to.path !=='/sys-lock'
) {next
({path
:'/sys-lock'
}); } else {next
(); } }else
{store
.commit
('goToLogin'
); } } } });/<code>
(1)當用戶打開 localhost,此時還沒有 token,匹配的是空路由,我們重定向到登錄頁 next({ path: '/login' });(2)用戶在登錄頁刷新頁面,也會進入路由攔截,此時匹配的是 login 路由,而 login 路由是不需要登錄驗證的(requiresAuth 為空或者 false),所以直接跳過執行 next();(3)用戶在登錄頁輸入了用戶名和密碼,登錄成功,保存了 token,跳轉到 /home 路由;(4)此時進入路由攔截,已經有 token了,但是還沒有用戶權限 permissionList,然後發請求獲取用戶權限列表,得到權限後 next({ path: to.path, query: to.query }); 繼續往下走;(5)再次進入路由攔截,此時有 token 和 permissionList 了,就可以根據實際業務進行跳轉了。上面的代碼是判斷當前是不是 login 路由,如果用戶登錄後手動在地址欄輸入 /login,則清除 token 跳轉到登錄頁。其他的邏輯就跟具體業務相關了,就不細講了。
4、處理用戶權限
處理用戶權限,在 store.js 定義一個模塊 permission.js,專門用於處理用戶權限相關的邏輯,用戶權限列表、菜單列表都保存在此模塊;來看看 permission.js 主要做了什麼:
<code>import
httpRequestfrom
'@/assets/js/service/http'
;import
handleModulefrom
'@/assets/js/common/handle-module'
;import
router, { dynamicRoutes }from
'@/router/index'
;import
permissionRouterfrom
'@/router/router'
;export
default
{actions
: {async
FETCH_PERMISSION({ commit, state }) { commit('setPermission'
, []);let
data =await
getUserByToken();let
userPopedoms = data.userPopedoms || [];let
userPopeList = userPopedoms.filter(v
=> v.requestMapping !=='user-manage'
&& v.requestMapping !=='login'
); commit('setUserPopedoms'
, userPopeList);let
routes = handleModule.getRouter(userPopedoms, permissionRouter);let
homeContainer = dynamicRoutes.find(v
=> v.path ==='/'
); homeContainer.children = routes.concat(homeContainer.children); homeContainer.redirect = homeContainer.children[0
].name;let
sidebarMenu = handleModule.getSidebarMenu(userPopeList); commit('setMenu'
, sidebarMenu);let
initialRoutes = router.options.routes; router.addRoutes(dynamicRoutes); commit('setPermission'
, [...initialRoutes, ...dynamicRoutes]); } }, };/<code>
(1)首先,let data = await getUserByToken(); 發請求獲取用戶權限,得到 data,data.userPopedoms 格式大致如下:
<code>[ {"moduleGroupId"
:1001
,"moduleGroupName"
:"部署管理"
,"requestMapping"
:"deploy-manage"
, }, {"moduleGroupId"
:1100
,"moduleGroupName"
:"系統管理"
,"requestMapping"
:"sys-manage"
,"moduleList"
: [ {"moduleId"
:1101
,"moduleName"
:"系統日誌"
,"requestMapping"
:"system-log"
,"moduleGroupId"
:1100
, }, {"moduleId"
:1102
,"moduleName"
:"系統告警"
,"requestMapping"
:"sys-alert"
,"moduleGroupId"
:1100
, }, ], } ]/<code>
(2)然後,根據我們寫好的路由數組,進行對比,過濾得到我們要的路由。路由格式在上文“路由定義”的 router/router.js 已經提到。還要根據用戶權限處理得到側邊欄菜單。
為此,我們需要兩個處理函數,一個根據用戶權限列表和路由數組過濾得到最終路由,另一個根據用戶權限處理得到側邊欄菜單。所以另外專門創建了一個文件 handle-module.js 存放這兩個函數。
<code>const
handleModule = { getRouter(permissionList = [], allRouter = []) {let
permissions = permissionList.reduce((
acc, cur
) => {if
(cur.moduleList && cur.moduleList.length >0
) cur = cur.moduleList;return
acc.concat(cur); }, []).map(v
=> v.requestMapping);return
allRouter.filter(item
=> permissions.includes(item.meta.permitName)); }, getSidebarMenu(permissionList = []) {let
sidebarMenu = []; permissionList.forEach(item
=> {let
menuItem = {name
: item.requestMapping,title
: item.moduleGroupName, }; menuItem.children = (item.moduleList || []).map(child
=> ({name
: child.requestMapping,title
: child.moduleName, })); sidebarMenu.push(menuItem); });return
sidebarMenu; } };export
default
handleModule;/<code>
(3)上面得到過濾後的路由數組後,加入到 path 為 '/' 的 children 下面
<code>{path
:'/'
,name
:'home'
,component
:()
=>import
('@/views/home.vue'
),meta
: {requiresAuth
:true
, },children
: [ {path
:'/user-info'
,name
:'user-info'
,component
:()
=>import
('@/views/user-setting/user-info.vue'
), }, ] }/<code>
(4)上面根據權限生成側邊欄菜單之後,保存在 store 待用。
(5)上面第三步將動態路由加入到 home 的 children 之後,就可以將 dynamicRoutes 加入到路由中了。router.addRoutes(dynamicRoutes);
(6)到了這裡,路由就添加完了,也就是 FETCH_PERMISSION 操作完畢了,就可以在 action.then 裡面調用 next({ path: to.path, query: to.query }); 進去路由,也就是進入 home。我們上面已經將 home 路由重定向為菜單的第一個路由信息,所以會進入系統菜單的第一個頁面。
刷新頁面後,根據 router.beforeEach 的判斷,有 token 但是沒有 permissionList ,會重新觸發 action 去發請求獲取用戶權限,之前的邏輯會重新走一遍,所以沒有問題。
退出登錄後,需要清除 token 並刷新頁面。因為是通過 addRoutes 添加路由的,而 vue-router 沒有刪除路由的 api,所以清除路由、清除 store 中存儲的各種信息,刷新頁面是最保險的。
相關文件的目錄截圖:
四、總結
缺點:全局路由守衛裡,每次路由跳轉都要做判斷;每次刷新頁面,需要重新發請求獲取用戶權限;退出登錄時,需要刷新一次頁面將動態添加的路由以及權限信息清空;
優點:菜單與路由分離,菜單的修改、添加、刪除由後端控制,利於後期維護;使用 addRoutes 動態掛載路由,可控制用戶不能在 url 輸入相關地址進行跳轉;
vue權限管理還有其他實現方式,大家可以根據實際業務考慮做調整,以上的實現方式是比較適合我們現有項目的需求的。以上,有問題歡迎提出交流,喜歡的話點個贊哦~
推薦Vue學習資料文章:
《聊聊昨晚尤雨溪現場針對Vue3.0 Beta版本新特性知識點彙總》
《【新消息】Vue 3.0 Beta 版本發佈,你還學的動麼?》
《Vue + Koa從零打造一個H5頁面可視化編輯器——Quark-h5》
《深入淺出Vue3 跟著尤雨溪學 TypeScript 之 Ref 【實踐】》
《手把手教你深入淺出vue-cli3升級vue-cli4的方法》
《Vue 3.0 Beta 和React 開發者分別槓上了》
《手把手教你用vue drag chart 實現一個可以拖動 / 縮放的圖表組件》
《Vue3 嚐鮮》
《手把手讓你成為更好的Vue.js開發人員的12個技巧和竅門【實踐】》
《
Vue 開源項目 TOP45》《2020 年,Vue 受歡迎程度是否會超過 React?》
《手把手教你Vue解析pdf(base64)轉圖片【實踐】》
《手把手教你Vue之父子組件間通信實踐講解【props、$ref 、$emit】》
《深入淺出Vue3 的響應式和以前的區別到底在哪裡?【實踐】》
《
乾貨滿滿!如何優雅簡潔地實現時鐘翻牌器(支持JS/Vue/React)》《基於Vue/VueRouter/Vuex/Axios登錄路由和接口級攔截原理與實現》
《手把手教你D3.js 實現數據可視化極速上手到Vue應用》
《吃透 Vue 項目開發實踐|16個方面深入前端工程化開發技巧【上】》
《吃透 Vue 項目開發實踐|16個方面深入前端工程化開發技巧【中】》
《吃透 Vue 項目開發實踐|16個方面深入前端工程化開發技巧【下】》
作者:lxcan
轉發鏈接:https://segmentfault.com/a/1190000022431839