perf: 优化动态路由

This commit is contained in:
kailong321200875 2023-08-27 09:03:39 +08:00
parent 1452a1afc7
commit 879358821d
14 changed files with 118 additions and 98 deletions

View File

@ -31,11 +31,7 @@ If you need a basic template, please switch to the `mini` branch. `mini` simply
- [vue-element-plus-admin](https://element-plus-admin.cn/) - Full version of the github site - [vue-element-plus-admin](https://element-plus-admin.cn/) - Full version of the github site
- [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - Full version of the gitee site - [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - Full version of the gitee site
account: **admin/admin test/test** account: **admin/admin**
`admin` account is used to simulate the control permission of the server, and render whatever the server returns
`test` account is used to simulate the front-end control authority. The server only returns the menu key to be displayed, and the front-end performs matching rendering
Online examples do not apply to menu filtering by default, but directly use Static routing Online examples do not apply to menu filtering by default, but directly use Static routing

View File

@ -31,11 +31,7 @@ vue-element-plus-admin 的定位是后台集成方案,不太适合当基础模
- [vue-element-plus-admin](https://element-plus-admin.cn/) - 完整版 github 站点 - [vue-element-plus-admin](https://element-plus-admin.cn/) - 完整版 github 站点
- [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - 完整版 gitee 站点 - [vue-element-plus-admin](https://kailong110120130.gitee.io/vue-element-plus-admin) - 完整版 gitee 站点
帐号:**admin/admin test/test** 帐号:**admin/admin**
`admin` 帐号用于模拟服务端控制权限,服务端返回什么就渲染什么
`test` 帐号用于模拟前端控制权限,服务端只返回需要显示的菜单 key前端进行匹配渲染
在线例子默认不适用菜单过滤,而是直接使用静态路由表 在线例子默认不适用菜单过滤,而是直接使用静态路由表

View File

@ -1070,12 +1070,41 @@ export default [
url: '/role/list', url: '/role/list',
method: 'get', method: 'get',
timeout, timeout,
response: ({ query }) => { response: () => {
const { roleName } = query
return { return {
data: { data: {
code: code, code: code,
data: roleName === 'admin' ? adminList : testList data: adminList
}
}
}
},
{
url: '/role/table',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: {
list: List,
total: 4
}
}
}
}
},
// 列表接口
{
url: '/role/list2',
method: 'get',
timeout,
response: () => {
return {
data: {
code: code,
data: testList
} }
} }
} }

View File

@ -30,5 +30,5 @@ export const getAdminRoleApi = (
} }
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => { export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
return request.get({ url: '/role/list', params }) return request.get({ url: '/role/list2', params })
} }

View File

@ -115,6 +115,14 @@ const dynamicRouterChange = (show: boolean) => {
appStore.setDynamicRouter(show) appStore.setDynamicRouter(show)
} }
//
const serverDynamicRouter = ref(appStore.getServerDynamicRouter)
const serverDynamicRouterChange = (show: boolean) => {
ElMessage.info(t('setting.reExperienced'))
appStore.setServerDynamicRouter(show)
}
// //
const fixedMenu = ref(appStore.getFixedMenu) const fixedMenu = ref(appStore.getFixedMenu)
@ -206,6 +214,11 @@ watch(
<ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" /> <ElSwitch v-model="dynamicRouter" @change="dynamicRouterChange" />
</div> </div>
<div class="flex justify-between items-center">
<span class="text-14px">{{ t('setting.serverDynamicRouter') }}</span>
<ElSwitch v-model="serverDynamicRouter" @change="serverDynamicRouterChange" />
</div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-14px">{{ t('setting.fixedMenu') }}</span> <span class="text-14px">{{ t('setting.fixedMenu') }}</span>
<ElSwitch v-model="fixedMenu" @change="fixedMenuChange" /> <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />

View File

@ -1,15 +1,21 @@
import { isArray, isObject } from '@/utils/is' // 获取传入的值的类型
const getValueType = (value: any) => {
const type = Object.prototype.toString.call(value)
return type.slice(8, -1)
}
export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => { export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionStorage') => {
const setStorage = (key: string, value: any) => { const setStorage = (key: string, value: any) => {
window[type].setItem(key, isArray(value) || isObject(value) ? JSON.stringify(value) : value) const valueType = getValueType(value)
window[type].setItem(key, JSON.stringify({ type: valueType, value }))
} }
const getStorage = (key: string) => { const getStorage = (key: string) => {
const value = window[type].getItem(key) const value = window[type].getItem(key)
try { if (value) {
return JSON.parse(value || '') const { value: val } = JSON.parse(value)
} catch (error) { return val
} else {
return value return value
} }
} }
@ -18,8 +24,17 @@ export const useStorage = (type: 'sessionStorage' | 'localStorage' = 'sessionSto
window[type].removeItem(key) window[type].removeItem(key)
} }
const clear = () => { const clear = (excludes?: string[]) => {
window[type].clear() // 获取排除项
const keys = Object.keys(window[type])
const defaultExcludes = ['dynamicRouter', 'serverDynamicRouter']
const excludesArr = excludes ? [...excludes, ...defaultExcludes] : defaultExcludes
const excludesKeys = excludesArr ? keys.filter((key) => !excludesArr.includes(key)) : keys
// 排除项不清除
excludesKeys.forEach((key) => {
window[type].removeItem(key)
})
// window[type].clear()
} }
return { return {

View File

@ -93,7 +93,9 @@ export default {
footer: 'Footer', footer: 'Footer',
uniqueOpened: 'Unique opened', uniqueOpened: 'Unique opened',
tagsViewIcon: 'Tags view icon', tagsViewIcon: 'Tags view icon',
dynamicRouter: 'Dynamic router', // 开启动态路由
dynamicRouter: 'Enable dynamic router',
serverDynamicRouter: 'Server dynamic router',
reExperienced: 'Please exit the login experience again', reExperienced: 'Please exit the login experience again',
fixedMenu: 'Fixed menu' fixedMenu: 'Fixed menu'
}, },

View File

@ -93,7 +93,8 @@ export default {
footer: '页脚', footer: '页脚',
uniqueOpened: '菜单手风琴', uniqueOpened: '菜单手风琴',
tagsViewIcon: '标签页图标', tagsViewIcon: '标签页图标',
dynamicRouter: '动态路由', dynamicRouter: '开启动态路由',
serverDynamicRouter: '服务端动态路由',
reExperienced: '请重新退出登录体验', reExperienced: '请重新退出登录体验',
fixedMenu: '固定菜单' fixedMenu: '固定菜单'
}, },

View File

@ -5,16 +5,12 @@ import type { RouteRecordRaw } from 'vue-router'
import { useTitle } from '@/hooks/web/useTitle' import { useTitle } from '@/hooks/web/useTitle'
import { useNProgress } from '@/hooks/web/useNProgress' import { useNProgress } from '@/hooks/web/useNProgress'
import { usePermissionStoreWithOut } from '@/store/modules/permission' import { usePermissionStoreWithOut } from '@/store/modules/permission'
// import { useDictStoreWithOut } from '@/store/modules/dict'
import { usePageLoading } from '@/hooks/web/usePageLoading' import { usePageLoading } from '@/hooks/web/usePageLoading'
// import { getDictApi } from '@/api/common'
const permissionStore = usePermissionStoreWithOut() const permissionStore = usePermissionStoreWithOut()
const appStore = useAppStoreWithOut() const appStore = useAppStoreWithOut()
// const dictStore = useDictStoreWithOut()
const { getStorage } = useStorage() const { getStorage } = useStorage()
const { start, done } = useNProgress() const { start, done } = useNProgress()
@ -30,14 +26,6 @@ router.beforeEach(async (to, from, next) => {
if (to.path === '/login') { if (to.path === '/login') {
next({ path: '/' }) next({ path: '/' })
} else { } else {
// if (!dictStore.getIsSetDict) {
// // 获取所有字典
// const res = await getDictApi()
// if (res) {
// dictStore.setDictObj(res.data)
// dictStore.setIsSetDict(true)
// }
// }
if (permissionStore.getIsAddRouters) { if (permissionStore.getIsAddRouters) {
next() next()
return return
@ -45,15 +33,14 @@ router.beforeEach(async (to, from, next) => {
// 开发者可根据实际情况进行修改 // 开发者可根据实际情况进行修改
const roleRouters = getStorage('roleRouters') || [] const roleRouters = getStorage('roleRouters') || []
const userInfo = getStorage(appStore.getUserInfo)
// 是否使用动态路由 // 是否使用动态路由
if (appStore.getDynamicRouter) { if (appStore.getDynamicRouter) {
userInfo.role === 'admin' appStore.serverDynamicRouter
? await permissionStore.generateRoutes('admin', roleRouters as AppCustomRouteRecordRaw[]) ? await permissionStore.generateRoutes('server', roleRouters as AppCustomRouteRecordRaw[])
: await permissionStore.generateRoutes('test', roleRouters as string[]) : await permissionStore.generateRoutes('frontEnd', roleRouters as string[])
} else { } else {
await permissionStore.generateRoutes('none') await permissionStore.generateRoutes('static')
} }
permissionStore.getAddRouters.forEach((route) => { permissionStore.getAddRouters.forEach((route) => {

View File

@ -21,6 +21,7 @@ interface AppState {
fixedHeader: boolean fixedHeader: boolean
greyMode: boolean greyMode: boolean
dynamicRouter: boolean dynamicRouter: boolean
serverDynamicRouter: boolean
pageLoading: boolean pageLoading: boolean
layout: LayoutType layout: LayoutType
title: string title: string
@ -42,7 +43,6 @@ export const useAppStore = defineStore('app', {
mobile: false, // 是否是移动端 mobile: false, // 是否是移动端
title: import.meta.env.VITE_APP_TITLE, // 标题 title: import.meta.env.VITE_APP_TITLE, // 标题
pageLoading: false, // 路由跳转loading pageLoading: false, // 路由跳转loading
breadcrumb: true, // 面包屑 breadcrumb: true, // 面包屑
breadcrumbIcon: true, // 面包屑图标 breadcrumbIcon: true, // 面包屑图标
collapse: false, // 折叠菜单 collapse: false, // 折叠菜单
@ -57,11 +57,12 @@ export const useAppStore = defineStore('app', {
fixedHeader: true, // 固定toolheader fixedHeader: true, // 固定toolheader
footer: true, // 显示页脚 footer: true, // 显示页脚
greyMode: false, // 是否开始灰色模式,用于特殊悼念日 greyMode: false, // 是否开始灰色模式,用于特殊悼念日
dynamicRouter: getStorage('dynamicRouter') || true, // 是否动态路由 dynamicRouter: getStorage('dynamicRouter'), // 是否动态路由
fixedMenu: getStorage('fixedMenu') || false, // 是否固定菜单 serverDynamicRouter: getStorage('serverDynamicRouter'), // 是否服务端渲染动态路由
fixedMenu: getStorage('fixedMenu'), // 是否固定菜单
layout: getStorage('layout') || 'classic', // layout布局 layout: getStorage('layout') || 'classic', // layout布局
isDark: getStorage('isDark') || false, // 是否是暗黑模式 isDark: getStorage('isDark'), // 是否是暗黑模式
currentSize: getStorage('default') || 'default', // 组件尺寸 currentSize: getStorage('default') || 'default', // 组件尺寸
theme: getStorage('theme') || { theme: getStorage('theme') || {
// 主题色 // 主题色
@ -138,6 +139,9 @@ export const useAppStore = defineStore('app', {
getDynamicRouter(): boolean { getDynamicRouter(): boolean {
return this.dynamicRouter return this.dynamicRouter
}, },
getServerDynamicRouter(): boolean {
return this.serverDynamicRouter
},
getFixedMenu(): boolean { getFixedMenu(): boolean {
return this.fixedMenu return this.fixedMenu
}, },
@ -216,6 +220,10 @@ export const useAppStore = defineStore('app', {
setStorage('dynamicRouter', dynamicRouter) setStorage('dynamicRouter', dynamicRouter)
this.dynamicRouter = dynamicRouter this.dynamicRouter = dynamicRouter
}, },
setServerDynamicRouter(serverDynamicRouter: boolean) {
setStorage('serverDynamicRouter', serverDynamicRouter)
this.serverDynamicRouter = serverDynamicRouter
},
setFixedMenu(fixedMenu: boolean) { setFixedMenu(fixedMenu: boolean) {
setStorage('fixedMenu', fixedMenu) setStorage('fixedMenu', fixedMenu)
this.fixedMenu = fixedMenu this.fixedMenu = fixedMenu

View File

@ -1,34 +0,0 @@
import { defineStore } from 'pinia'
import { store } from '../index'
export interface DictState {
isSetDict: boolean
dictObj: Recordable
}
export const useDictStore = defineStore('dict', {
state: (): DictState => ({
isSetDict: false,
dictObj: {}
}),
getters: {
getDictObj(): Recordable {
return this.dictObj
},
getIsSetDict(): boolean {
return this.isSetDict
}
},
actions: {
setDictObj(dictObj: Recordable) {
this.dictObj = dictObj
},
setIsSetDict(isSetDict: boolean) {
this.isSetDict = isSetDict
}
}
})
export const useDictStoreWithOut = () => {
return useDictStore(store)
}

View File

@ -1,6 +1,10 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { asyncRouterMap, constantRouterMap } from '@/router' import { asyncRouterMap, constantRouterMap } from '@/router'
import { generateRoutesFn1, generateRoutesFn2, flatMultiLevelRoutes } from '@/utils/routerHelper' import {
generateRoutesByFrontEnd,
generateRoutesByServer,
flatMultiLevelRoutes
} from '@/utils/routerHelper'
import { store } from '../index' import { store } from '../index'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
@ -34,17 +38,17 @@ export const usePermissionStore = defineStore('permission', {
}, },
actions: { actions: {
generateRoutes( generateRoutes(
type: 'admin' | 'test' | 'none', type: 'server' | 'frontEnd' | 'static',
routers?: AppCustomRouteRecordRaw[] | string[] routers?: AppCustomRouteRecordRaw[] | string[]
): Promise<unknown> { ): Promise<unknown> {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
let routerMap: AppRouteRecordRaw[] = [] let routerMap: AppRouteRecordRaw[] = []
if (type === 'admin') { if (type === 'server') {
// 模拟后端过滤菜单 // 模拟后端过滤菜单
routerMap = generateRoutesFn2(routers as AppCustomRouteRecordRaw[]) routerMap = generateRoutesByServer(routers as AppCustomRouteRecordRaw[])
} else if (type === 'test') { } else if (type === 'frontEnd') {
// 模拟前端过滤菜单 // 模拟前端过滤菜单
routerMap = generateRoutesFn1(cloneDeep(asyncRouterMap), routers as string[]) routerMap = generateRoutesByFrontEnd(cloneDeep(asyncRouterMap), routers as string[])
} else { } else {
// 直接读取静态路由表 // 直接读取静态路由表
routerMap = cloneDeep(asyncRouterMap) routerMap = cloneDeep(asyncRouterMap)

View File

@ -38,7 +38,7 @@ export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormal
} }
// 前端控制路由生成 // 前端控制路由生成
export const generateRoutesFn1 = ( export const generateRoutesByFrontEnd = (
routes: AppRouteRecordRaw[], routes: AppRouteRecordRaw[],
keys: string[], keys: string[],
basePath = '/' basePath = '/'
@ -69,7 +69,7 @@ export const generateRoutesFn1 = (
if (isUrl(item) && (onlyOneChild === item || route.path === item)) { if (isUrl(item) && (onlyOneChild === item || route.path === item)) {
data = Object.assign({}, route) data = Object.assign({}, route)
} else { } else {
const routePath = onlyOneChild ?? pathResolve(basePath, route.path) const routePath = (onlyOneChild ?? pathResolve(basePath, route.path)).trim()
if (routePath === item || meta.followRoute === item) { if (routePath === item || meta.followRoute === item) {
data = Object.assign({}, route) data = Object.assign({}, route)
} }
@ -78,7 +78,11 @@ export const generateRoutesFn1 = (
// recursive child routes // recursive child routes
if (route.children && data) { if (route.children && data) {
data.children = generateRoutesFn1(route.children, keys, pathResolve(basePath, data.path)) data.children = generateRoutesByFrontEnd(
route.children,
keys,
pathResolve(basePath, data.path)
)
} }
if (data) { if (data) {
res.push(data as AppRouteRecordRaw) res.push(data as AppRouteRecordRaw)
@ -88,7 +92,7 @@ export const generateRoutesFn1 = (
} }
// 后端控制路由生成 // 后端控制路由生成
export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => { export const generateRoutesByServer = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {
const res: AppRouteRecordRaw[] = [] const res: AppRouteRecordRaw[] = []
for (const route of routes) { for (const route of routes) {
@ -111,7 +115,7 @@ export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRe
} }
// recursive child routes // recursive child routes
if (route.children) { if (route.children) {
data.children = generateRoutesFn2(route.children) data.children = generateRoutesByServer(route.children)
} }
res.push(data as AppRouteRecordRaw) res.push(data as AppRouteRecordRaw)
} }
@ -121,7 +125,7 @@ export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRe
export const pathResolve = (parentPath: string, path: string) => { export const pathResolve = (parentPath: string, path: string) => {
if (isUrl(path)) return path if (isUrl(path)) return path
const childPath = path.startsWith('/') || !path ? path : `/${path}` const childPath = path.startsWith('/') || !path ? path : `/${path}`
return `${parentPath}${childPath}`.replace(/\/\//g, '/') return `${parentPath}${childPath}`.replace(/\/\//g, '/').trim()
} }
// 路由降级 // 路由降级

View File

@ -220,7 +220,7 @@ const signIn = async () => {
if (appStore.getDynamicRouter) { if (appStore.getDynamicRouter) {
getRole() getRole()
} else { } else {
await permissionStore.generateRoutes('none').catch(() => {}) await permissionStore.generateRoutes('static').catch(() => {})
permissionStore.getAddRouters.forEach((route) => { permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访 addRoute(route as RouteRecordRaw) // 访
}) })
@ -241,17 +241,16 @@ const getRole = async () => {
const params = { const params = {
roleName: formData.username roleName: formData.username
} }
// admin -
// test -
const res = const res =
formData.username === 'admin' ? await getAdminRoleApi(params) : await getTestRoleApi(params) appStore.getDynamicRouter && appStore.getServerDynamicRouter
? await getAdminRoleApi(params)
: await getTestRoleApi(params)
if (res) { if (res) {
const routers = res.data || [] const routers = res.data || []
setStorage('roleRouters', routers) setStorage('roleRouters', routers)
appStore.getDynamicRouter && appStore.getServerDynamicRouter
formData.username === 'admin' ? await permissionStore.generateRoutes('server', routers).catch(() => {})
? await permissionStore.generateRoutes('admin', routers).catch(() => {}) : await permissionStore.generateRoutes('frontEnd', routers).catch(() => {})
: await permissionStore.generateRoutes('test', routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => { permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访 addRoute(route as RouteRecordRaw) // 访