本文略長,建議收藏。
線上網站演示效果地址:http://ashuai.work:8891/home
GitHub倉庫地址:https://github.com/shuirongsh...
所謂的許可權,其實指的就是:使用者是否能看到,以及是否允許其對資料進行增刪改查的操作
,因為現在開發專案的主流方式是前後端分離,所以整個專案的許可權是後端許可權控制
搭配前端許可權控制
共同實現的
後端許可權
1. 知道是哪個使用者(角色)發的請求
- token判斷(前端請求頭中傳過去,透過請求攔截器)
- userid判斷(前端請求頭中傳過去,透過請求攔截器)
- cookie判斷(用的少)
- session判斷(用的很少)
2. 許可權設計模式
RBAC模式~基於角色的許可權訪問控制(Role-Based Access Control)
是商業系統中最常見的許可權管理技術之一。
關於什麼是RBAC模式
的概念本文這裡不做贅述,大家可以這樣理解:
2.1 RBAC模式
- 某個系統有一些人在使用,但是使用這個系統的這些人分為兩個派系。有老闆派(老闆、老闆小姨子、老闆小舅子等),也有打工仔派(張三、李四、王二、麻子)。
- 系統有一些頁面用於展示資料,作為老闆派的相關工作人員,肯定是哪個頁面都能夠看到,畢竟公司都是老闆的,必須什麼都能看到
- 而作為打工仔派的工作人員,肯定是許可權比較低了,只能看到一個業績頁面(假設業績頁面記錄了自己每個月為公司創造的業績)
- 這個案例中的老闆派和打工仔派就是兩個角色,而老闆、老闆小姨子、老闆小舅子和張三、李四、王二、麻子這些使用者就是隸屬於角色的使用者
- 使用者是角色的具象化體現
- 使用者的數量一般都大於角色的數量的(想一下...)
- 所以我們在做許可權控制的時候,只需要控制角色能看到那個頁面即可,至於使用者就讓他隸屬於這個角色便是
- 透過使用者和角色進行關聯,也做到了複用和解耦
- 角色能看到哪個頁面,取決於角色和選單頁面進行的關聯(透過勾選選單樹關聯)
步驟如下:
- 給對應角色賦予(分配)相應選單許可權
- 讓新建的使用者隸屬於某個角色,就是給新建的使用者分配一下角色
2.2 後端建表
正常情況下需要五張表:
- 選單表(用於儲存系統的選單資訊,如選單的名字、點選這個選單要去跳轉的路由url、以及這個選單的icon圖示名、選單id等...)
- 角色表(用於儲存系統的角色資訊,如角色名、角色id、角色備註等...)
- 角色選單表(用於儲存某個角色能看到選單id有哪些,一個角色id對應多個選單id,一對多關係)
- 使用者表(用於儲存使用者隸屬於哪個角色,比如老闆小舅子就是隸屬於老闆角色,以及使用者名稱、使用者id、使用者備註啥的...)
- 組織表(用於記錄角色屬於哪個組織,再大一些的系統專案會建此表,小專案沒有也行)
三張表也能用
我們將角色選單表,糅在角色表中,這樣的話,只要新建選單表、角色表(包含角色選單資訊)、使用者表,根據使用者的使用者名稱和密碼進行登入(後端會根據使用者名稱和密碼查詢到這個使用者隸屬於那個角色下,從而返回此使用者對應角色的選單資訊資料)
本文演示建兩張表
為了更好的便於大家理解,本文只新建兩張表,一張是選單表,另一張是角色表(角色表中存此角色能看到的單id),而登入的時候,大家選擇角色登入,其實選擇使用者登入從某種意義上來說,也是相當於角色登入。
第一步:
角色登入發請求,後端返回此角色對應的選單樹資料(需要前端進一步加工一下),前端獲取選單樹資料以後,將其存到vuex
中比如是menuTree
陣列欄位,el-menu
元件再取到vuex
中的menuTree
陣列資料使用,根據這個選單樹資料進行自動遞迴渲染
關於el-menu元件的遞迴自呼叫,可以參見筆者之前的這篇文章:https://segmentfault.com/a/11... 若對於元件遞迴知識忘了,可以參見筆者的這篇文章:https://segmentfault.com/a/11...
第二步:
第一步中,完成了el-menu
元件的渲染展示,但是還少了路由表樹結構陣列資料(因為點選選單需要進行路由跳轉),所以我們還需要再搞一份資料routerTree
,這個routerTree
資料是根據後端返回的選單樹資料,加工成符合路由表樹結構的陣列,這個陣列是給路由表使用的
routerTree
的值是動態,因為不同角色的routerTree
不一樣- 對應的路由表還有靜態的,不變的,比如
404
路由、login
路由、首頁home
路由 - 靜態路由前端直接寫死即可,動態路由使用
router.addRoutes(routerTree)
將其新增到路由表中即可
至於重新整理頁面
vuex
中資料丟失,就重發一次請求,或者本地存一份都行的實際上,重新整理頁面,並不是
vuex
中的資料丟失,而是,而是vuex
中的資料初始化了(回到最初的樣子,最初時,vuex中的資料本來就是空的)
透過上述兩步驟,一個許可權系統的基本樣子結構就出來了,只不過還有一些細節需要處理,這個請繼續往下閱讀
前端許可權細化控制
前端許可權細化分類大致可以分為四種:
- 選單的許可權控制
- 頁面的許可權控制
- 按鈕的許可權控制
- 欄位的許可權控制
1.選單的許可權控制(以左側導航選單為例)
不同角色的使用者登入以後,看到的是不同的選單內容。比如:
- 普通角色(的使用者),只能看到某一個選單
- 管理員角色能看到所有的選單
- 以
el-menu
選單元件進行舉例說明
2.頁面的許可權控制
- 角色沒登入時,手動在位址列輸入
url
地址跳轉,就強制使用者跳轉到登入頁 - 角色登入後,手動在位址列輸入不存在的
url
(或者自己不能看的url
)地址跳轉,讓其跳轉404頁面
- 某些特殊的頁面,還可以使用
vue-router
中的beforeRouteEnter
再進行細化控制
假設打工仔張三,想要去看老闆的頁面,因為自己登入時,後端返回的選單樹中沒有老闆頁面的資料,所以路由表中也沒有老闆頁面的路由,所以位址列
url
直接跳轉時,就會跳轉到一個不存在的路由頁面,就不會顯示出東西了。關於路由,大家也可以這樣理解:就是當位址列中輸入對應的
path
即url
時,路由表大佬
去做對應匹配,匹配到了以後,再去渲染對應的.vue元件
從而呈現對應的資料內容(匹配不到那就404
唄)
3.按鈕的許可權控制
按鈕的控制,可以細分為兩塊:一是 : 是否能看到這個按鈕、另外是 : 能看到不過是否能點選這個按鈕(按鈕是否禁用)
- 按鈕是否展示,取決於是否給角色分配了按鈕許可權。
- 如有的只能看到
檢視按鈕
,有的增刪改查按鈕
都可以看到 - 再一個,我們可以把按鈕當做是一個特殊的選單頁面
- 最後,要有一個規則限制,新增節點時,可以新增選單節點,也可以新增按鈕節點,只不過按鈕節點永遠是最底層的位置,不能在按鈕節點下再去新增頁面(無意義操作)
- 不理解上面那句話,請繼續往下閱讀
注意,在專案中最好不要使用禁用按鈕去控制許可權,如果角色使用者沒有某個按鈕的許可權,直接刪除這個節點即可,比如
v-if
,或el.parentNode.removeChild(el)
,因為使用禁用按鈕去控制許可權存在一定風險,如筆者的這篇文章:https://segmentfault.com/a/11...特殊情況下,使用禁用按鈕也可以去分配控制許可權,具體情況具體分析
4. 欄位的許可權控制
如下的表格:
姓名 | 年齡 | 家鄉 | 愛好 |
---|---|---|---|
孫悟空 | 500 | 花果山 | 大鬧天宮 |
比如年齡欄位是隱私,只有老闆能看到員工的年齡,老闆的小姨子和小舅子都不能看到,這個需求的實現可以前端根據角色id進行過濾;或者後端再建表做對映關係,返回給前端。
篇幅原因,這裡不贅述了。後續空閒了,筆者再寫一篇介紹
後端建選單表和角色表
我們先從後端開始寫,關於每個欄位的意思,筆者在程式碼中提到了
1. 選單表
兩張表都在程式碼中,大家閱讀完文章以後,可以在GitHub
倉庫中自行獲取
1.1選單表資料庫截圖
這裡有幾個欄位需要著重介紹一下:
1.2pid
欄位
pid
欄位,即為:parentId
是父級節點欄位,就是當前節點的父節點,後端在資料庫中儲存前端選單樹資料時,是不會存成樹結構的資料的(JSON形式除外,但這種方式很少用)後端儲存的資料是:把樹結構的資料鋪平(拍平)
以後的資料。
比如我們有這樣一個樹結構資料
let treeData = [
{
id: 1,
name: '中國',
children: [ // 有的後端喜歡使用child欄位,一個意思
{
id: 3,
name: '北京',
},
{
id: 4,
name: '上海',
children: [
{
id: 6,
name: '浦東新區'
}
]
},
]
},
{
id: 2,
name: '美國',
children: [ // 有的後端喜歡使用child欄位,一個意思
{
id: 5,
name: '紐約',
},
]
},
]
資料庫中可不會直接存一個樹結構,資料庫會把樹結構拍平存起來,即這樣儲存:
pid | id | name |
---|---|---|
0 | 1 | 中國 |
1 | 3 | 北京 |
1 | 4 | 上海 |
4 | 6 | 浦東新區 |
0 | 2 | 美國 |
2 | 5 | 紐約 |
注意!資料庫中不需要儲存
children
欄位,children
欄位是一個虛擬欄位,是當後端同事查詢選單表資料結構時,將扁平化的資料轉成樹結構時,遞迴程式碼建立的,並返回給前端。實際上,後端同事可以直接將
sql
語句查詢到的扁平化資料庫資料陣列直接丟給前端,至於加工成樹結構,也可以由前端同事去加工,不過正常情況下,扁平化資料轉成樹結構資料都是後端做的,不過前端也要會寫相應遞迴函式
也就是相當於這樣的JSON
[
{
"pid": 0,
"id": 1,
"name": "中國",
},
{
"pid": 1,
"id": 3,
"name": "北京",
},
{
"pid": 1,
"id": 4,
"name": "上海",
},
{
"pid": 4,
"id": 6,
"name": "浦東新區",
},
{
"pid": 0,
"id": 2,
"name": "美國",
},
{
"pid": 2,
"id": 5,
"name": "紐約",
}
]
至於樹結構如何拍平的,可以筆者寫了兩個遞迴函式大家可以看看,函式寫法二的可讀性更好一些哦
函式寫法一:
// 拍平加pid欄位
function pidFn(data, sqlArr = [], pid = 0) { // 假設頂級的pid為0
data.forEach((item) => { // 遍歷得到每一項
let obj = JSON.parse(JSON.stringify(item)) // 深複製一份
obj.pid = pid // 給每一項都賦值pid
delete obj.children // 拍平不需要children
sqlArr.push(obj) // 丟到sqlArr陣列中
if (item.children) { // 有子節點就遞迴操作
pidFn(item.children, sqlArr, item.id) // 當前項的id就是子項的pid
}
})
return sqlArr // 一波操作,最後再丟出來即可
}
let res = pidFn(treeData)
console.log('拍平加父id欄位', res);
函式寫法二:
function pidFn(data) {
let sqlArr = [] // 定義一個陣列用於儲存拍平後的資料
function digui(data, pid) { // 專門定義個遞迴函式使用者清晰的儲存資料
data.forEach((item) => { // 遍歷樹結構資料
let obj = JSON.parse(JSON.stringify(item)) // 深複製一份
obj.pid = pid // 給每一項都賦值pid
delete obj.children // 拍平了,就不需要children了
sqlArr.push(obj) // 丟到sqlArr陣列中
if (item.children) { // 如果樹結構有子節點就繼續遞迴操作
// 當前節點的id就是子節點的pid
digui(item.children, item.id) // 遞迴函式接收樹結構資料,以後pid引數
}
})
}
digui(data, 0) // 遞迴函式初次執行,假設頂級pid是0
return sqlArr // 將遞迴的結果丟出去
}
let res = pidFn(treeData)
console.log('拍平加父id欄位', res);
1.3pids
欄位
pids
欄位是所有的父級節點的組合的陣列,比如一個三級節點,它的pid
是二級節點的id
,而它的pids
是所有的父級節點,包括二級節點id
和一級節點id
。
當然資料庫儲存,不能直接存一個陣列進去,所以toString()
轉成字串儲存即可
1.4cUrl
欄位
cUrl
即componentUrl
元件的地址的意思。就是路由表中用於讀取元件的component
函式。比如有以下一個路由表:
{
path: "/welcome",
name: "welcome",
component: resolve => require(["@/views/pages/welcome.vue"], resolve),
},
在資料庫中儲存為:
url | cUrl | ... |
---|---|---|
/welcome | /pages/welcome.vue | ... |
表示:當位址列的url
值為/welcome
時,去讀取並渲染@/views/pages/welcome.vue
檔案,即做到了url
和頁面
的對應關係
其他的欄位含義,看上述圖片的註釋即可理解
1.5選單表sql程式碼
DROP TABLE IF EXISTS `menus`;
CREATE TABLE `menus` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '唯一id',
`pid` int(11) NOT NULL COMMENT '上級父節點的id,即為parentId(注意,children欄位是不用儲存,children欄位是遞迴時,新增進去的)',
`pids` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '上級節點的id陣列轉的字串',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '樹節點的名字',
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '即為選單的path',
`cUrl` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '當訪問url時,前端路由需要讀取並渲染的.vue檔案的路徑,一般是相對於views裡的',
`type` int(255) NULL DEFAULT NULL COMMENT 'type為1是選單,為2是按鈕',
`icon` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '選單的圖示名',
`sort` int(255) NULL DEFAULT NULL COMMENT '選單的上下排序',
`status` int(255) NULL DEFAULT NULL COMMENT '是否開啟欄位,1是開啟,2是關閉',
`isHidden` int(255) NULL DEFAULT NULL COMMENT '是否隱藏選單,1是顯示,2是隱藏',
`isCache` int(255) NULL DEFAULT NULL COMMENT '是否快取,1是快取,2是不快取',
`remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '備註',
`isDel` int(255) NULL DEFAULT 1 COMMENT '刪除標識,1代表未刪除可用,2代表已刪除不可用',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 105 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Compact;
2. 角色表
2.1角色表資料庫截圖
2.2menuIds欄位
實際專案中,會有一個角色選單表
用於做角色和選單的對映關係,這裡筆者直接糅在一塊了,便於大家理解。
2.3角色表sql程式碼
DROP TABLE IF EXISTS `roles`;
CREATE TABLE `roles` (
`roleId` int(255) NOT NULL AUTO_INCREMENT COMMENT '每一個角色的id',
`roleName` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '每一個角色的name名字',
`roleRemark` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL COMMENT '角色的備註',
`menuIds` text CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL COMMENT '當前的這個角色能看到(勾選)的選單的id(給角色賦予選單)',
PRIMARY KEY (`roleId`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Compact;
前端使用角色去登入控制頁面的許可權
1. 登入頁(選擇角色發請求獲取對應選單樹資料)
1.1效果圖
1.2程式碼思路
el-select
點選下拉框
<el-select
v-model="value"
placeholder="請選擇角色登入"
@visible-change="
(flag) => {
if (flag) {
listRoles();
}
}
"
>
<el-option
v-for="item in roleList"
:key="item.value"
:label="item.roleName + ' (' + item.roleRemark + ')'"
:value="item.menuIds"
>
</el-option>
</el-select>
發請求獲取角色列表有哪些
async listRoles() {
const res = await this.$auth.listRoles();
if (res.code == 0) {
this.roleList = res.data;
}
}
介面返回角色列表有以下資料
[
{
"roleId": 29,
"roleName": "超級管理員",
"roleRemark": "能看到所有",
"menuIds": "1,19,21,42,30,31,41,22,32,33,27,34,39,40,35,36,37,38,100,101,99,25,26"
},
{
"roleId": 30,
"roleName": "前端",
"roleRemark": "只能看前端相關",
"menuIds": "1,19,21,42,30,31,41,22,32,33,27,34,39,40,35"
},
{
"roleId": 31,
"roleName": "後端",
"roleRemark": "只能看後端相關",
"menuIds": "1,36,37,38,100,101"
}
]
當我們選擇以超級管理員登入時,就根據超級管理員roleId為29
對應的能看到的menuIds
有哪些的選單,去查詢對應的選單資料。
/auth/roleMenuByMenuId?menuIds=1,19,21,42,30,31,41,22,32,33,27,34,39,40,35,36,37,38,100,101,99,25,26
後端介面如下:
// 根據角色id查詢能看到的選單有哪些(前提是這些選單是啟用狀態的)
route.get('/roleMenuByMenuId', (req, res) => {
// 1. 接收前端傳的引數
let menuIds = req.query.menuIds
// 2. 拼接sql語句準備去資料庫查詢
let sql = `SELECT * FROM menus WHERE id IN (${menuIds}) AND isDel = 1 AND status = 1`
// 資料庫連線池建立連線去資料庫中撈資料
pool.getConnection(function (err, connection) {
if (err) { throw err }
connection.query(sql, function (error, results, fields) {
connection.release()
let apiRes = {
code: 0,
msg: "成功",
// 注意!注意!注意!
// 這時候撈到的資料還是扁平化的陣列
// 需要將扁平化的陣列轉成樹結構
// 所以這裡定義了一個changeTree函式用於加工資料
data: changeTree(results, 0, 0)
}
res.send(apiRes)
})
})
})
如果大家對於express寫介面,做資料庫查詢忘了,可以看下筆者之前寫的一篇全棧文章複習一下遺忘的知識:https://segmentfault.com/a/11...
列印查到的results
結果:扁平化陣列資料截圖:
所以需要有個函式方法(工具類),能夠將扁平化結構轉成樹結構,函式如下:
/**
* 想要將扁平化陣列轉成樹結構,首先必須知道頂級的pid是啥(0)
* 第一步,假設我們只需要找頂級的這一項,
* 只需要對比一下那一項的pid是這個pid即可
* 而後遞迴即可
* */
// 透過pid將扁平化的陣列轉成樹結構。給樹結構新增level欄位(資料庫中沒存,當然存也可以)
module.exports = function changeTree(arr, pid, level) {
let treeArr = []
level = level + 1 // 新增層級欄位
arr.forEach((item, index) => {
if (item.pid == pid) {
// 把不是這一項的剩餘幾項,都丟到這個children陣列裡面,再進行遞迴(這一項已經確定了,是父級,沒必要再遞迴了)
let restArr = arr.filter((item, index) => { return item.pid != pid })
item['children'] = changeTree(restArr, item.id, level) // 這裡需要進行傳id當做pid(因為自己的id就是子節點的pid)
if (item.children.length == 0) { delete item.children } // 加一個判斷,若是children: [] 即沒有內容就刪除,不返回給前端children欄位
item['level'] = level // 新增層級欄位
// 操作完以後,把一整個的都追加到陣列中去
treeArr.push(item)
}
});
return treeArr
}
呼叫:changeTree(results, 0, 0)
這裡要告知頂級的pid
,這裡我假設為0
,同時level
層級從第0層
開始
經過這樣一加工再返回給前端,就是一個樹結構的資料了,如下圖:
這樣的話,前端就可以使用了
因為業務簡單,所以這裡筆者的後端express沒有分層處理
關於後端的分層,前端同事可以這樣簡單理解:
控制層
接收引數
校驗引數
處理引數
.....
業務層
資料處理查、改什麼的...
持久層
生成sql語句
資料庫操作
資料返回
2.加工後端返回的選單樹資料存在vuex中以供使用
2.1登入成功跳轉
async loginIn() {
// vuex中發相應的請求
const res = await this.$store.dispatch("menu/tree_menu", this.value);
if (res.code == 0) {
// 存一份登入的角色名
let i = this.roleList.findIndex((item) => {
return item.menuIds == this.value;
});
sessionStorage.setItem("username", this.roleList[i].roleName);
// 然後登入成功去跳轉
this.$message({
type: "success",
message: "登入成功",
});
this.$router.push({ path: "/" });
}
},
2.2vuex中的操作~el-menu需要的選單樹
- 注意,上述圖中,後端是返回了選單樹,但是!但是!依舊是不能直接使用!
- 原因是前端還需要再進行一次加工,將選單樹加工成兩個樹結構的資料
- 一個是
menuTree: [], // el-menu需要的選單樹
- 另一個是
routerTree: [] // vue-router需要的路由樹
el-menu
選單樹需要的menuTree
的值是不包含按鈕節點的,而因為按鈕被當做了特殊的頁面節點,所以這裡需要過濾一下vue-router
需要的路由樹routerTree
也要根據後端返回的選單樹進行加工,加工成具有component
函式屬性值的路由樹資料
我們先看el-menu
選單樹需要的menuTree
資料的加工函式
// 加工後端返的樹結構資料 是給選單遞迴元件el-menu使用的
export function setElMenuTreeWithoutBtn(oldTree, newTree) {
oldTree.forEach((item) => {
let newTreeObj = {
...item,
children: null
}
if (item.children) { // 有子集內容,且子集內容為選單type=1才去遞迴(按鈕type=2就不要了,這樣就過濾了...)
if (item.children[0].type == 1) {
setElMenuTreeWithoutBtn(item.children, newTreeObj.children = [])
}
}
if (newTreeObj.children == null) { // 為null說明沒有子集,或者子集都是按鈕被忽略了,刪除之
delete newTreeObj.children
}
newTree.push(newTreeObj)
})
return newTree
}
呼叫此方法是在vuex
中的登入邏輯中
const state = {
isCollapse: false,
menuTree: [], // el-menu需要的選單樹
routerTree: [] // vue-router需要的路由樹
};
const mutations = { ... }
const actions = {
// 相當於login登入介面
tree_menu({ commit, dispatch }, menuIds) {
return new Promise((resolve, reject) => {
roleMenuByMenuId(menuIds)
.then(
(res) => {
let menuTree = res.data[0].children // 頂級節點PC不要
commit('TREE_MENU', setElMenuTreeWithoutBtn(menuTree, [])) // 加工選單樹給到el-menu元件使用
commit('ROUTE_TREE', setRouterTree(menuTree, [])) // 加工選單樹給到路由表使用
sessionStorage.setItem("token", "token"); // 模擬token儲存
sessionStorage.setItem("menuIds", menuIds); // 存一份當前角色的menuIds
setBtnAuth(menuTree) // 設定按鈕
resolve(res) // 丟擲去告知結果
}
).catch(...)
})
}
}
最終加工好的menuTree
樹結構資料,會被el-menu
元件使用:
注意,el-menu
元件放在檢視層元件中:
讓我們來看一下layout
資料夾中的index.vue
元件是如何使用menuTree
資料的吧
layout-->index.vue
<el-menu
ref="elMenu"
:collapse="isCollapse"
:default-active="activeIndex"
class="elMenu"
background-color="#333"
text-color="#B0B0B2"
active-text-color="#fff"
:unique-opened="false"
router
@select="menuSelect"
>
<!-- 普通選單(前端寫死固定,如首頁、歡迎頁等) -->
<el-menu-item index="/">
<i class="el-icon-s-home"></i>
<span slot="title">首頁</span>
</el-menu-item>
<!-- 遞迴動態選單(後端返回,不同角色選單不一致) -->
<myitem :data="menuArr"></myitem>
</el-menu>
import myitem from "./components/myitem.vue";
components: { myitem }
data(){
return {
menuArr: this.$store.state.menu.menuTree
}
}
再複習一下遞迴元件
layout-->components-->myitem.vue
<template>
<div>
<template v-for="(item, index) in data">
<!-- isHidden值等於1才去顯示,等於2隱藏 -->
<template v-if="item.isHidden == 1">
<!-- 因為有子集和無子集渲染html標籤不一樣,所以要分為兩種情況:情況一:有子集的情況:-->
<template v-if="item.children">
<!-- 有子集去遞迴顯示 -->
<el-submenu :key="index" :index="item.url">
<template slot="title">
<i class="el-icon-platform-eleme"></i>
<span>{{ item.name }}</span>
</template>
<myitem :data="item.children"></myitem>
</el-submenu>
</template>
<!-- 情況二:沒子集的情況 -->
<template v-else>
<!-- 沒子集直接顯示內容即可 -->
<el-menu-item :key="index" :index="item.url">
<i class="el-icon-eleme"></i>
<span slot="title">{{ item.name }}</span>
</el-menu-item>
</template>
</template>
</template>
</div>
</template>
<script>
export default {
name: "myitem",
props: {
data: {
type: Array,
default: [],
},
},
// 注意: 在template標籤上使用v-for,:key="index"不能寫在template標籤上,因為其標籤不會被渲染,會引起迴圈錯誤
};
</script>
關於元件的name屬性
,其實不寫也行,不過當需要使用到元件的遞迴自呼叫或使用keep-alive快取元件
時,就得加上了。當然也有別的方案...
到目前為止,選單能夠顯示了,但是點選選單卻是空白頁,因為少了路由表樹結構資料了
2.3vuex中的操作~vue-router需要的路由樹
路由表中的路由分為靜態路由(固定路由)和動態路由(後端根據不同的使用者/角色返回的路由),比如可以有以下的靜態固定路由:
// 固定的靜態路由,比如登入頁、首頁、404頁等...
const staticRoutes = [
{
path: '/',
// component: resolve => require(["@/layout/index.vue"], resolve),
component: Layout, // 二者一個意思
redirect: '/home',
children: [
{
path: "/home",
name: "home",
component: resolve => require(["@/views/home.vue"], resolve),
},
]
},
{
path: '/login',
component: resolve => require(["@/views/login.vue"], resolve),
},
{
path: '/404',
component: resolve => require(["@/views/404.vue"], resolve),
},
// { path: '*', redirect: '/404' }
]
這裡有一個坑,404頁面的匹配兜底路由
在最後,但是不能直接寫在靜態路由中的最後一個,會重新整理自動到404頁面了。因為是動態路由的做法,所以404頁面的匹配兜底路由
拼接在vue-router
的路由樹陣列資料中即可。所以上述的{ path: '*', redirect: '/404' }
筆者註釋掉了。
登入獲取到後端返回的樹結構資料後,加工,給路由表使用
// vuex中
roleMenuByMenuId(menuIds).then(
(res) => {
let menuTree = res.data[0].children // 頂級節點PC不要
commit('ROUTE_TREE', setRouterTree(menuTree, []))
}
)
setRouterTree
加工函式
// 加工後端返的樹結構資料 是給vue-router路由表使用
import Layout from '@/layout/index.vue'
let page404 = { path: '*', redirect: '/404' }
export function setRouterTree(oldTree, newTree) {
oldTree.forEach((item) => {
let newTreeObj = {
path: item.level == 2 ? `/${item.url}` : `${item.url}`,
name: item.name,
component(resolve) { require([`@/views${item.cUrl}`], resolve) },
meta: {
title: item.name
}
}
if (item.level == 2) { // 如果是二級,就統一使用layout元件檢視層
// newTreeObj['component'] = Layout // 這兩個一個意思
newTreeObj['component'] = (resolve) => {
require(["@/layout/index.vue"], resolve)
}
}
if (item.children) {
if (item.children[0].type == 1) { // 路由表樹結構也不需要按鈕哦,只要type等於1的選單
setRouterTree(item.children, newTreeObj.children = [])
}
}
newTree.push(newTreeObj)
});
return newTree.concat(page404) // 將404頁面拼接到最後面,做通配路由使用
}
列印加工好的路由樹結果:
注意箭頭指向的地方,component
函式要去引用指向解析元件哦,這個一定要有
所以commit('ROUTE_TREE', setRouterTree(menuTree, []))
的值給到路由表去使用
那麼?路由表如何使用加工好的這個路由表樹資料呢?使用vue
的addRoutes
方法新增即可,在登入成功以後。加工好以後,直接新增即可
import router from "@/router";
import store from "@/store";
async loginIn() {
const res = await this.$store.dispatch("menu/tree_menu", this.value);
if (res.code == 0) {
/**
* 登入成功以後也要動態新增一下路由tip1,或者非同步重載入一下tip2
* 否則會出現首次登入router.beforeEach中動態路由方法不觸發
* 即首次登入點選動態選單部分出現空白頁(重新整理後正常)
* */
router.addRoutes(store.state.menu.routerTree); // tip1: 注掉效果明顯
let i = this.roleList.findIndex((item) => {
return item.menuIds == this.value;
});
sessionStorage.setItem("username", this.roleList[i].roleName);
this.$message({
type: "success",
message: "登入成功",
});
this.$router.push({ path: "/" });
// setTimeout(() => { // tip2:注掉效果明顯
// location.reload();
// }, 10);
}
},
最後一步,重新整理頁面時,vuex
初始化,所以在beforeEach
鉤子函式中重新發請求,獲取選單樹資料即可
// 路由全域性攔截
router.beforeEach((to, from, next) => {
// 去登入頁面肯定放行的,管他有沒有token
if (to.path === '/login') {
next()
}
// 去的不是登入頁面,再看看有沒有token認證
else {
const token = sessionStorage.getItem('token')
if (!token) { // 沒token,就讓其回到登入頁登入
next({ path: "/login" })
} else { // 有token,再看看有沒有選單路由資訊
if (store.state.menu.routerTree.length > 0) { // 有選單資訊,就放行
next()
} else { // 沒有選單資訊,就再發一次請求獲取選單資訊
let menuIds = sessionStorage.getItem('menuIds')
store.dispatch('menu/tree_menu', menuIds).then((res) => {
if (res.code == 0) {
router.addRoutes(store.state.menu.routerTree)
next({ ...to, replace: true }) // 確保動態路由已被完全新增進去了
}
})
}
}
}
})
前端使用角色去登入控制按鈕的許可權
按鈕的許可權思路,在登入時,根據選單樹轉成一個按鈕樹,有按鈕的許可權,就為true,沒有就直接沒有。
然後再定義函式去獲取某某頁面下的某某按鈕的值是否為true,為true就說明有許可權,為false就說明沒有許可權,可以使用函式引入方式,或者自定義指令方式。
將選單樹的按鈕轉成key/value布林值按鈕樹形式
目標按鈕樹結構
{
"前端框架": {
"vue頁面": {
"新增vue": true,
"編輯vue": true,
"刪除vue": true,
"佔位按鈕": true
},
"react頁面": {
"新增react": true,
"編輯react": true
},
"angular": {
"agl1": {
"agl1新增/編輯": true,
"agl1刪除": true
},
"agl2": {}
}
},
"後端框架": {
"springBoot": {},
"myBatis": {},
"特工001": {},
"特工002": {}
},
"系統設定": {
"角色管理": {},
"選單管理": {}
}
}
定義函式去設定
依舊是在登入的時候,去根據選單樹去設定按鈕樹
roleMenuByMenuId(menuIds).then(
(res) => {
let menuTree = res.data[0].children // 頂級節點PC不要
//......
setBtnAuth(menuTree) // 設定按鈕樹
resolve(res) // 丟擲去告知結果
}
)
設定按鈕樹遞迴函式
// 按鈕許可權設定
export function setBtnAuth(tree, btnAuthObj = {}) {
// 迴圈加工
tree.forEach((item) => {
if (item.type == 1) { // 型別為1說明是選單
btnAuthObj[item.name] = {} // 選單就加上一個物件屬性
if (item.children) { // 若有子集,就遞迴加物件屬性
// 因為物件是引用型別,所以直接賦值整個按鈕許可權物件就都有了
setBtnAuth(item.children, btnAuthObj[item.name])
}
}
if (item.type == 2) { // 型別為2說明是按鈕
btnAuthObj[item.name] = true // 按鈕的賦值true表示有按鈕許可權
}
})
// 加工完畢以後,丟出去以供使用
sessionStorage.setItem('btnAuthObj', JSON.stringify(btnAuthObj))
return btnAuthObj
}
獲取按鈕樹遞迴函式
// 按鈕許可權獲取
export function getBtnAuth(whichPage, btnName) { // 查詢:那個頁面下的什麼按鈕名字是否有許可權
let flag // 找到了,才說明有許可權
function getBtn(whichPage, btnName, btnAuthObj = JSON.parse(sessionStorage.getItem('btnAuthObj'))) {
for (const key in btnAuthObj) {
if (key == whichPage) {
flag = btnAuthObj[key][btnName]
} else {
getBtn(whichPage, btnName, btnAuthObj[key])
}
}
}
getBtn(whichPage, btnName) // 遞迴查詢標識賦值
return flag ? true : false // 找到了為true,沒找到undefined,這裡再判斷一下,返回布林值
}
函式使用方式
<el-button type="danger" v-if="isShowDeleteBtn">刪除vue</el-button>
computed: {
isShowDeleteBtn() {
return getBtnAuth("vue頁面", "刪除vue");
},
},
自定義指令方式
<el-button type="primary" v-btn="{ vue頁面: '新增vue' }">新增vue</el-button>
<el-button type="primary" v-btn="{ vue頁面: '編輯vue' }">編輯vue</el-button>
自定義指令v-btn
import { getBtnAuth } from "@/utils";
export default {
inserted(el, binding, vnode) {
const whichPage = Object.keys(binding.value)[0]
const btnName = Object.values(binding.value)[0]
let flag = getBtnAuth(whichPage, btnName)
if (!flag) {
el.parentNode.removeChild(el)
}
},
unbind(el, binding, vnode) {
}
}
相當於去查詢,查 某個頁面
中有沒有 某個按鈕
共用的動態路由元件
比如好幾個頁面,都是同一個結構內容,只是id不同,這個時候就需要使用動態路由元件了傳參
/dynaOne/:001
<template>
<div>
<span>共用的動態元件:</span>
<h2>
根據 <span class="val">{{ val }}</span> 的不同發相應請求
</h2>
</div>
</template>
<script>
export default {
data() {
return {
val: Object.values(this.$route.params)[0].slice(1),
/*
注意url的配置:
思路是多個路由path使用同一個元件,需要加上監聽,解決路由變了,元件不重新整理的問題
身份標識id直接url中拼接即可使用
url:'xxx/:001'
*/
};
},
watch: {
$route: {
handler: function () {
this.val = Object.values(this.$route.params)[0].slice(1);
},
},
},
};
</script>
<style>
.val {
color: brown;
font-size: 36px;
margin: 0 8px;
}
</style>
總結
- 透過這種方式,就可以做到動態選單得了。
- 但是紙上得來終覺淺,還是得實操
- 又因為篇幅原因,筆者關於一些細節文章中沒有提到
- 所以大家可以去筆者釋出出的網站上看效果
- 以及star一下筆者的倉庫
- 將程式碼拉下來跑起來,一點點捋清程式碼思路
- 這樣印象才會深刻
比如後端介面如何寫,前端選單樹
el-tree
勾選傳參細節問題,詳情,請看程式碼另外,後臺管理系統,都有
tab
標籤頁,動態路由的tab標籤頁
在三層選單中會出現快取問題
,後續筆者不忙了,會繼續更新這個倉庫的哦。等等...
大家可以隨意新增角色,並賦予選單許可權,不過不能編輯選單資料哦。筆者為了降低自己的資料不被打亂了
如果幫到了您,還請賞賜一個star
,也是我們更文的動力。(PS:寫文章真的要花費不少時間哦?)