關於後臺管理系統的路由,想花一點時間,完全的整理一份實現動態路由的點點滴滴。前端
首先聲明,這篇文章是基於花褲衩大神的《手摸手,帶你用vue擼後臺》,在他項目的基礎上,幫助想要實現動態路由的小夥伴,來寫的一篇使用筆記。vue
咱們在開發後臺管理系統的過程當中,會有不一樣的人來操做系統,有admin(管理員)、superAdmin(超管),還會有各類運營人員、財務人員。爲了區別這些人員,咱們會給不一樣的人分配不同的角色,從而來展現不一樣的菜單,這個就必需要經過動態路由來實現。vue-router
簡單聊一下兩種方式的優點,畢竟若是你歷來沒作過,說再多也看不明白,仍是得看代碼vuex
一、不用後端幫助,路由表維護在前端
二、邏輯相對比較簡單,比較容易上手
複製代碼
一、相對更安全一點
二、路由表維護在數據庫
複製代碼
花褲衩大神的方案是前端控制,他的核心是經過路由的meta屬性,經過role來控制路由的加載。具體的實現方案:數據庫
一、根據登陸用戶的帳號,返回前端用戶的角色
二、前端根據用戶的角色,跟路由表的meta.role進行匹配
三、講匹配到的路由造成可訪問路由
複製代碼
具體的代碼邏輯:segmentfault
一、把靜態路由和動態路由分別寫在router.js
二、在vuex維護一個state,經過配角色來控制菜單顯不顯示
三、新建一個路由守衛函數,能夠在main.js,也能夠抽離出來一個文件
四、側邊欄的能夠從vuex裏面取數據來進行渲染
複製代碼
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Layout from '@/layout'
// constantRoutes 靜態路由,主要是登陸頁、404頁等不須要動態的路由
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path*',
component: () => import('@/views/redirect/index')
}
]
},
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error-page/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/error-page/401'),
hidden: true
}
]
// asyncRoutes 動態路由
export const asyncRoutes = [
{
path: '/permission',
component: Layout,
redirect: '/permission/page',
alwaysShow: true,
name: 'Permission',
meta: {
title: 'Permission',
icon: 'lock',
// 核心代碼,能夠經過配的角色來進行遍歷,從而是否展現
// 這個意思就是admin、editor這兩個角色,這個菜單是能夠顯示
roles: ['admin', 'editor']
},
children: [
{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'PagePermission',
meta: {
title: 'Page Permission',
// 這個意思就是隻有admin能展現
roles: ['admin']
}
}
]
}
]
const createRouter = () => new Router({
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// 這個是重置路由用的,頗有用,別看這麼幾行代碼
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher
}
export default router
複製代碼
import { asyncRoutes, constantRoutes } from '@/router'
// 這個方法是用來把角色和route.meta.role來進行匹配
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}
// 這個方法是經過遞歸來遍歷路由,把有權限的路由給遍歷出來
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
// 這個地方維護了兩個狀態一個是addRouters,一個是routes
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
accessedRoutes = asyncRoutes || []
} else {
// 核心代碼,把路由和獲取到的角色(後臺獲取的)傳進去進行匹配
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
// 把匹配完有權限的路由給set到vuex裏面
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
複製代碼
這裏面的代碼主要是控制路由跳轉以前,先查一下有哪些可訪問的路由,登陸之後跳轉的邏輯能夠在這個地方寫後端
// permission.js
router.beforeEach((to, from, next) => {
if (store.getters.token) { // 判斷是否有token
if (to.path === '/login') {
next({ path: '/' });
} else {
// 判斷當前用戶是否已拉取完user_info信息
if (store.getters.roles.length === 0) {
store.dispatch('GetInfo').then(res => { // 拉取info
const roles = res.data.role;
// 把獲取到的role傳進去進行匹配,生成能夠訪問的路由
store.dispatch('GenerateRoutes', { roles }).then(() => {
// 動態添加可訪問路由表(核心代碼,沒有它啥也幹不了)
router.addRoutes(store.getters.addRouters)
// hack方法 確保addRoutes已完成
next({ ...to, replace: true })
})
}).catch(err => {
console.log(err);
});
} else {
next() //當有用戶權限的時候,說明全部可訪問路由已生成 如訪問沒權限的全面會自動進入404頁面
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登陸白名單,直接進入
next();
} else {
next('/login'); // 不然所有重定向到登陸頁
}
}
})
複製代碼
核心代碼是從router取能夠用的路由對象,來進行側邊欄的渲染,不論是前端動態加載仍是後端動態加載路由,這個代碼都是同樣的數組
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
// 把取到的路由進行循環做爲參數傳給子組件
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
// 獲取有權限的路由
routes() {
return this.$router.options.routes
}
複製代碼
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
}
複製代碼
前端控制路由,邏輯相對簡單,後端只須要存這個用戶的角色就能夠了,前端拿用戶的角色進行匹配。可是若是新增角色,就會很是痛苦,每個都要加。安全
後端控制路由是大部分後臺管理系統的解決方案,咱們公司也是經過這種方法管理路由的。具體的思路是這樣的:
一、用戶登陸之後,後端根據該用戶的角色,直接生成可訪問的路由數據,注意這個地方是數據
二、前端根據後端返回的路由數據,轉成本身須要的路由結構
複製代碼
具體的代碼邏輯:
一、router.js裏面只放一些靜態的路由,login、404之類
二、整理一份數據結構,存到表裏
三、從後端獲取路由數據,寫一個數據轉換的方法,講數據轉成可訪問的路由
四、也是維護一個vuex狀態,將轉換好的路由存到vuex裏面
五、側邊欄也是從路由取數據進行渲染
複製代碼
由於前段控制和後端控制,後面的流程大部分都是同樣的,因此這個地方只看看前面不同的流程:
GenerateRoutes({ commit }, data) {
return new Promise((resolve, reject) => {
getRoute(data).then(res => {
// 將獲取到的數據進行一個轉換,而後存到vuex裏
const accessedRouters = arrayToMenu(res.data)
accessedRouters.concat([{ path: '*', redirect: '/404', hidden: true }])
commit('SET_ROUTERS', accessedRouters)
resolve()
}).catch(error => {
reject(error)
})
})
}
複製代碼
咱們知道vue的router規定的數據結構是這樣的:
{
path: '/form',
component: Layout,
children: [
{
path: 'index',
name: 'Form',
component: () => import('@/views/form/index'),
meta: { title: 'Form', icon: 'form' }
}
]
}
複製代碼
因此,一級菜單有幾個參數必需要有:id、path、name、component、title,二級菜單children是一個數組,是子父級的關係,因此能夠給加一個fid或者parentId,來進行匹配,後面寫轉換方法的時候會詳細解釋,數據格式大概就是這樣:
// 一級菜單
// parentId爲0的就能夠當作一級菜單,id最好是能夠選4位數,至於爲何等你開發項目的時候就知道了
{
id: 1300
parentId: 0
title: "企業管理"
path: "/enterprise"
hidden: false
component: null
hidden: false
name: "enterprise"
},
// 二級菜單
// parentId不爲0的,就能夠拿parentId跟一級菜單的id去匹配,匹配上的就push到children裏面
{
id: 1307
parentId: 1300
title: "商戶信息"
hidden: false
path: "merchantInfo"
component: "enterprise/merchantInfo" // 要跟本地的文件地址匹配上
hidden: false
name: "merchantInfo"
}
複製代碼
剛纔獲取到的數據沒法直接轉成router進行渲染,須要一個arrayToMenu的方法,剛纔也說了一些思路,下面就一塊兒分析下這個方法:
export function arrayToMenu(array) {
const nodes = []
// 獲取頂級節點
for (let i = 0; i < array.length; i++) {
const row = array[i]
// 這個exists方法就是判斷下有沒有子級
if (!exists(array, row.parentId)) {
nodes.push({
path: row.path, // 路由地址
hidden: row.hidden, // 所有攜程true就行,若是後端沒配
component: Layout, // 通常就是匹配你文件的component
name: row.name, // 路由名稱
meta: { title: row.title, icon: row.name }, // title就是顯示的名字
id: row.id, // 路由的id
redirect: 'noredirect'
})
}
}
const toDo = Array.from(nodes)
while (toDo.length) {
const node = toDo.shift()
// 獲取子節點
for (let i = 0; i < array.length; i++) {
const row = array[i]
// parentId等於哪一個父級的id,就push到哪一個
if (row.parentId === node.id) {
const child = {
path: row.path,
name: row.name,
hidden: row.hidden,
// 核心代碼,由於二級路由的component是須要匹配頁面的
component: require('@/views/' + row.component + '/index.vue'),
meta: { title: row.title, icon: row.name },
id: row.id
}
if (node.children) {
node.children.push(child)
} else {
node.children = [child]
}
toDo.push(child)
}
}
}
return nodes
}
// 看下有沒有子級
function exists(rows, parentId) {
for (let i = 0; i < rows.length; i++) {
if (rows[i].id === parentId) return true
}
return false
}
複製代碼
側邊欄的代碼跟靜態的代碼是同樣的,就再也不說一遍了
動態路由到底是前端控制好,仍是後端控制好?只能說各有各的優點,畢竟業務場景也不同,你們能夠動手來試一下。代碼還有不少值得優化的地方,歡迎各位大神批評指正。
我的公衆號:小Jerry有話說
歡迎關注個人我的公衆號,一塊兒探討有趣的前端知識。