Vue3 的后臺(tái)管理的項(xiàng)目想要做個(gè)菜單的話熟掂,一般會(huì)使用 VueRouter 設(shè)置路由嗜侮,然后用UI庫的菜單組件(比如 el-menu)設(shè)置各種屬性稽犁,然后還需要考慮權(quán)限等問題沮翔。
這樣做雖然很靈活陨帆,但是對(duì)于簡(jiǎn)單項(xiàng)目就有點(diǎn)麻煩。那么我們能不能簡(jiǎn)化一下操作鉴竭,只設(shè)置一次歧譬,就可以實(shí)現(xiàn)菜單、路由外加權(quán)限的功能呢搏存?
技術(shù)棧 / 基礎(chǔ)工具
- vue3,script setup
- 動(dòng)態(tài)組件矢洲、異步組件
- window.history
- element-plus:el-menu璧眠、el-tabs
在線演示
https://naturefw.gitee.io/nf-rollup-ui-controller
可以來這里看看效果,好吧其實(shí)也沒啥特別的读虏,看效果也沒啥區(qū)別责静,只是使用的時(shí)候比較很方便。
源碼
https://gitee.com/naturefw/nf-rollup-ui-controller
思路
首先模仿 VueRouter 設(shè)置一個(gè)路由盖桥,這里簡(jiǎn)化一下灾螃,只考慮單層路由。然后再設(shè)置上菜單需要的屬性揩徊。
因?yàn)橐龆?jí)菜單腰鬼,所以再設(shè)置一個(gè)分組信息,另外再加上圖標(biāo)(icon)的設(shè)置塑荒,這樣基本就夠用了熄赡。
然后設(shè)置頁面布局,根據(jù)設(shè)置綁定菜單(el-menu)齿税,加載組件即可彼硫。
設(shè)置路由和菜單
import { defineAsyncComponent } from 'vue'
import home from './home.vue'
import { Loading, Connection, Edit, FolderOpened } from '@element-plus/icons'
// 基礎(chǔ)路徑
const baseUrl = '/nf-rollup-ui-controller'
/**
* 一級(jí)菜單,設(shè)計(jì)包含哪些二級(jí)菜單
*/
const group = [
{
id: 1,
title: '基礎(chǔ)控件',
icon: FolderOpened,
children: [
'base_html',
'base_ui',
'base_transmit',
'base_item'
]
},
{
id: 2,
title: '復(fù)合控件',
icon: FolderOpened,
children: [
'nf_form',
'nf_find',
'nf_grid',
'nf_button',
'nf_crud'
]
}
]
/**
* 二級(jí)菜單凌箕,設(shè)置路徑拧篮、標(biāo)題、圖標(biāo)和加載的組件
*/
const routes = {
base_html: {
path: '/base-html',
title: '原生HTML',
icon: Edit,
component: () => import('./ui/base/c-01html.vue')
},
base_ui: {
path: '/base-ui',
title: 'UI庫組件',
icon: Edit,
component: () => import('./ui/base/c-02UI.vue')
},
base_transmit: {
path: '/base-transmit',
title: '傳聲筒',
icon: '',
component: () => import('./ui/base/c-03transmit.vue')
}
}
設(shè)置基礎(chǔ)路徑
本地開發(fā)可以使用根目錄牵舱,但是項(xiàng)目發(fā)布后可能無法使用根目錄了串绩,這時(shí)候就需要設(shè)置一個(gè)基礎(chǔ)路徑,以應(yīng)對(duì)只能使用二級(jí)目錄的情況仆葡。預(yù)留權(quán)限接口
這里的 children 只是記錄了路由的 key 赏参,而不是完整的路由配置志笼,這樣設(shè)計(jì)是為了以后可以方便的加上權(quán)限,可以按照權(quán)限過濾掉沒有權(quán)限的路由(模塊)的key把篓,這樣加權(quán)限就方便多了纫溃。圖標(biāo)的設(shè)置方式
因?yàn)?element-plus 使用“組件”形式的圖標(biāo),所以還得用動(dòng)態(tài)組件的方式來加載圖標(biāo)韧掩,需要在設(shè)置的地方引入需要的圖標(biāo)紊浩,然后設(shè)置到菜單的屬性里面,這樣就可以實(shí)現(xiàn)圖標(biāo)的設(shè)置了疗锐。組件的設(shè)置
采用異步組件(defineAsyncComponent)來加載需要的組件坊谁,設(shè)置方式和 VueRouter 保持一致。
設(shè)計(jì)加載組件和刷新的處理方法
然后設(shè)計(jì)兩個(gè)函數(shù)滑臊,一個(gè)是加載組件的口芍,一個(gè)是頁面刷新后根據(jù) url路徑 加載組件的函數(shù)盈罐。
// 加載路由指定的組件
const getComponent = (key) => {
const route = routes[key]
if (route) {
// 設(shè)置標(biāo)題
document.title = route.title
// 設(shè)置url地址
window.history.pushState(null, null, baseUrl + route.path)
// 返回組件
return defineAsyncComponent(route.component)
} else {
return home
}
}
// 刷新時(shí)依據(jù)url加載組件
const refresh = (cb) => {
const path = window.location.pathname
if (path === '/' || path === baseUrl) {
// 首頁
} else {
const tmp = path.replace(baseUrl, '')
// 加載組件
for (const key in routes) {
const route = routes[key]
if (route.path === tmp) {
if (typeof cb === 'function'){
cb(key)
}
break
}
}
}
}
// 導(dǎo)出配置和函數(shù)
export {
group,
routes,
getComponent,
refresh
}
設(shè)置標(biāo)題和 URL 地址
點(diǎn)擊菜單抓艳,加載組件,順便設(shè)置一下瀏覽器的標(biāo)題和 URL 的路徑娜饵。
雖然現(xiàn)在瀏覽器都是標(biāo)簽的形式关划,沒有太大的空間顯示標(biāo)題小染,不過設(shè)置一下標(biāo)題也不麻煩。
然后用 window.history.pushState 設(shè)置一下瀏覽器的 URL 路徑贮折,這樣設(shè)置不會(huì)導(dǎo)致瀏覽器向服務(wù)器請(qǐng)求頁面裤翩。刷新自動(dòng)加載組件
刷新頁面后如果不做設(shè)置的話,是不會(huì)依據(jù) URL 加載對(duì)應(yīng)的組件的调榄,所以還需要我們寫個(gè)函數(shù)處理一下踊赠。
首先獲取 URL 的路徑(pathName),然后到路由設(shè)置里面查找對(duì)應(yīng)的組件振峻,然后加載即可臼疫。
這里做了一個(gè)回調(diào)函數(shù),可以更方便一些扣孟。
綁定菜單
做一個(gè)菜單的組件烫堤,比如叫做 menu.vue,引入上面的設(shè)置凤价,然后綁定到 el-menu 上面鸽斟。
<el-menu
class="el-menu-vertical-demo"
@select="select"
background-color="#6c747c"
text-color="#fff"
active-text-color="#ffd04b"
>
<el-sub-menu v-for="(item, index) in group"
:key="index"
:index="item.id"
>
<template #title>
<component
:is="item.icon"
style="width: 1.5em; height: 1.5em; margin-right: 8px;"
>
</component>
<span>{{item.title}}</span>
</template>
<el-menu-item v-for="(key, index) in item.children"
:key="index"
:index="key">
<template #title>
<component
:is="routes[key].icon"
style="width: 1.5em; height: 1.5em; margin-right: 8px;"
>
</component>
{{routes[key].title}}
</template>
</el-menu-item>
</el-sub-menu>
</el-menu>
使用動(dòng)態(tài)組件(component)加載圖標(biāo) ,其他的按照 el-menu 的要求進(jìn)行設(shè)置即可利诺,這樣一個(gè)簡(jiǎn)單的二級(jí)菜單就做好了富蓄。
然后設(shè)置一個(gè)組件屬性,用于傳遞選擇的菜單慢逾。
const props = defineProps({
events: Object
})
const events = props.events
// 二級(jí)菜單被選中
const select = (index, indexPath) => {
events.currIndex = index // 路由的key
}
設(shè)計(jì)頁面布局
采用 el-container 做頁面布局立倍,左面是菜單灭红,右面加載對(duì)應(yīng)的組件。
<el-container>
<el-aside width="200px">
<!--菜單-->
<nf-menu :events="events" />
</el-aside>
<el-main>
<!--二級(jí)導(dǎo)航-->
<component
:is="getComponent(events.currIndex)"
>
</component>
</el-main>
</el-container>
引入配置和函數(shù)口注,設(shè)置選擇的菜單的對(duì)象变擒,這樣就可以了。
const events = reactive({
currIndex: ''
})
refresh((key) => {
events.currIndex = key
})
加上權(quán)限過濾
設(shè)計(jì)權(quán)限的時(shí)候寝志,需要標(biāo)注可以訪問哪些菜單娇斑,也就是組件,然后設(shè)置好對(duì)應(yīng)的菜單(路由)的 key 即可材部。綁定的地方換成過濾后的數(shù)組即可毫缆。
實(shí)現(xiàn)多tab標(biāo)簽頁
上面的方法是實(shí)現(xiàn)了“單頁”的方式,點(diǎn)一個(gè)菜單加載一個(gè)組件乐导,再點(diǎn)一個(gè)菜單苦丁,替換掉原來的組件。
如果想做成 tab 標(biāo)簽頁的形式要怎么做呢物臂?
其實(shí)也很簡(jiǎn)單芬骄,我們只需要增加一個(gè) 數(shù)組 (Set,tabs) 用于存放點(diǎn)擊過的菜單的key鹦聪,然后依據(jù) tabs 綁定 el-tabs 即可。
- 設(shè)置容器和監(jiān)聽
const tabs = reactive(new Set([])) // 點(diǎn)擊過且沒有關(guān)閉的二級(jí)菜單蒂秘,做成動(dòng)態(tài)tab標(biāo)簽
// 監(jiān)聽當(dāng)前路由泽本,設(shè)置 tabs
watch(() =>currentRoute.key, (key) => {
tabs.add(key)
const route = this.routes[key] ?? {title: '首頁', path: '/'}
// 設(shè)置標(biāo)題
document.title = route.title
// 設(shè)置url地址
window.history.pushState(null, null, this.baseUrl + route.path)
})
- 綁定el-tabs
<el-tabs
v-model="currentRoute.key"
type="card"
>
<el-tab-pane
v-for="key in tabs"
:key="key"
:label="routes[key].title"
:name="key"
>
<template #label>
<span>{{routes[key].title}}
<circle-close-filled
style="width: 1.0em; height: 1.0em; margin-top: 8px;"
@click.stop="removeTab(key)" />
</span>
</template>
<component :is="routerControl[key]">
</component>
</el-tab-pane>
</el-tabs>
這樣就可以了。
把零散的代碼封裝成 函數(shù)庫 + 組件
基本功能測(cè)試通過姻僧,回顧一下代碼规丽,還是有一些麻煩,我們可以進(jìn)一步封裝一下:
- 路由+菜單的函數(shù)庫
- 菜單組件(nf-menu)
- 路由視圖——單頁(router-view)
- 路由視圖——tabs(router-view-tabs)
這樣封裝之后就方便多了撇贺,另外順便重構(gòu)一下代碼赌莺。
路由的函數(shù)庫
我們可以用 ES6 的 class 來定義,實(shí)現(xiàn)各種基礎(chǔ)功能松嘶。
import { defineAsyncComponent, reactive, watch, inject } from 'vue'
const flag = Symbol('nf-router-menu___')
/**
* 一個(gè)簡(jiǎn)單的路由
* @param {*} baseUrl 基礎(chǔ)路徑
* @param {*} routes 路由設(shè)置
* * {
* * base_html: { // 路由的 name
* * path: '/base-html',
* * title: '原生HTML',
* * icon: Edit,
* * component: () => import('./ui/base/c-01html.vue')
* * },
* * 其他路由設(shè)置
* * }
* @returns
*/
class Router {
constructor (info) {
// 設(shè)置當(dāng)前選擇的路由
this.currentRoute = reactive({
key: ''
})
this.baseUrl = info.baseUrl // 基礎(chǔ)路徑艘狭,應(yīng)對(duì)網(wǎng)站的二級(jí)目錄
this.home = info.home // 默認(rèn)的首頁
this.group = info.group // 一級(jí)菜單,分組翠订,設(shè)置包含的二級(jí)菜單(路由)
this.routes = info.routes // 路由設(shè)置巢音,二級(jí)菜單
/**
* 'key', 'key' // 保存路由的key(name)
*/
this.tabs = reactive(new Set([])) // 點(diǎn)擊過且沒有關(guān)閉的二級(jí)菜單,做成動(dòng)態(tài)tab標(biāo)簽
// 把路由里的組件轉(zhuǎn)換一下
this.routerControl = {}
this.setup()
}
/**
* 初始化設(shè)置
*/
setup = () => {
// 監(jiān)聽當(dāng)前路由尽超,設(shè)置 tabs
watch(() => this.currentRoute.key, (key) => {
if (key !== 'main' ) this.tabs.add(key)
const route = this.routes[key] ?? {title: '首頁', path: '/'}
// 設(shè)置標(biāo)題
document.title = route.title
// 設(shè)置url地址
window.history.pushState(null, null, this.baseUrl + route.path)
})
// 把路由里的組件轉(zhuǎn)換一下
for (const key in this.routes) {
const r = this.routes[key]
this.routerControl[key] = defineAsyncComponent(r.component)
}
}
// 加載路由指定的組件
getComponent = () => {
if (this.currentRoute.key === '') {
return this.home
} else {
const route = this.routes[this.currentRoute.key]
if (route) {
// 返回組件
return defineAsyncComponent(route.component)
}
}
}
// 刪除tab
removeTab = (key) => {
// 轉(zhuǎn)換為數(shù)組官撼,便于操作
const arr = Array.from(this.tabs)
if (arr.length === 1) {
// 只有一個(gè)tab,刪除后激活桌面
this.tabs.delete(key)
this.currentRoute.key = 'home'
return
}
// 判斷是否當(dāng)前tab似谁,如果是當(dāng)前tab傲绣,激活左面或者右面的tab
if (this.currentRoute.key === key) {
// 查找當(dāng)前tab的數(shù)組序號(hào)
const index = arr.indexOf(key)
// 判斷當(dāng)前tab的位置
if (index === 0) {
// 第一位掠哥,激活后面的
this.currentRoute.key = arr[1]
} else {
// 激活前面的
this.currentRoute.key = arr[index - 1]
}
}
// 刪除
this.tabs.delete(key)
}
// 刷新時(shí)依據(jù)url加載組件
refresh = () => {
const path = window.location.pathname
if (path === '/' || path === this.baseUrl) {
// 首頁
} else {
const tmp = path.replace(this.baseUrl, '')
// 驗(yàn)證路由
for (const key in this.routes) {
const route = this.routes[key]
if (route.path === tmp) {
this.currentRoute.key = key
break
}
}
}
}
}
/**
* 創(chuàng)建簡(jiǎn)易路由
*/
const createRouter = (info) => {
// 創(chuàng)建路由,
const router = new Router(info)
// 使用vue的插件秃诵,設(shè)置全局路由
return (app) => {
// 便于模板獲取
app.config.globalProperties.$router = router
// 便于代碼獲取
app.provide(flag, router)
}
}
// 在代碼里獲取路由
const useRouter = () => {
return inject(flag)
}
export {
createRouter,
useRouter
}
首先定義一個(gè)class续搀,存放需要的各種屬性和處理方法,然后做一個(gè)Vue的插件顷链,便于做初始化的設(shè)置和全局變量目代。最后做一個(gè)獲取路由的函數(shù)即可。
菜單組件:nf-menu
<el-menu
ref="domMenu"
class="el-menu-vertical-demo"
default-active="1"
@select="(index) => {$router.currentRoute.key = index}"
background-color="#6c747c"
text-color="#fff"
active-text-color="#ffd04b"
>
<el-sub-menu v-for="(item, index) in $router.group"
:key="index"
:index="item.id"
>
<template #title>
<component
:is="item.icon"
style="width: 1.5em; height: 1.5em; margin-right: 8px;"
>
</component>
<span>{{item.title}}</span>
</template>
<el-menu-item v-for="(key, index) in item.children"
:key="index"
:index="key">
<template #title>
<component
:is="$router.routes[key].icon"
style="width: 1.5em; height: 1.5em; margin-right: 8px;"
>
</component>
{{$router.routes[key].title}}
</template>
</el-menu-item>
</el-sub-menu>
</el-menu>
封裝的不夠細(xì)致嗤练,主要目的是可以偷懶用著方便就好榛了。
不求靈活,只求方便煞抬,簡(jiǎn)單粗暴的實(shí)現(xiàn)功能即可霜大。
單頁路由視圖:router-view
<template>
<component :is="$router.getComponent()">
</component>
</template>
這個(gè)就很簡(jiǎn)單了,用動(dòng)態(tài)組件加載需要的組件即可革答。
動(dòng)態(tài)tab標(biāo)簽頁:router-view-tabs
<el-tabs
v-model="$router.currentRoute.key"
type="card"
>
<el-tab-pane label="桌面" :name="main">
<component :is="$router.home">
</component>
</el-tab-pane>
<el-tab-pane
v-for="key in $router.tabs"
:key="key"
:label="$router.routes[key].title"
:name="key"
>
<template #label>
<span>{{$router.routes[key].title}}
<circle-close-filled
style="width: 1.0em; height: 1.0em; margin-top: 8px;"
@click.stop="$router.removeTab(key)" />
</span>
</template>
<component :is="$router.routerControl[key]">
</component>
</el-tab-pane>
</el-tabs>
這個(gè)稍微復(fù)雜一點(diǎn)战坤,用屬性綁定 el-tabs,設(shè)置屬性残拐、圖標(biāo)和事件 途茫。
在項(xiàng)目里的使用方法
首先定義一個(gè)路由,然后在 main.js 里面掛載溪食。然后布局里面加載組件即可囊卜。
- 定義路由和菜單
/router/index.js
import { Loading, Connection, Edit, FolderOpened } from '@element-plus/icons'
import { createRouter } from '/nf-ui-core'
import home from '../views/home.vue'
export default createRouter({
/**
* 基礎(chǔ)路徑
*/
baseUrl: '/nf-rollup-ui-controller',
/**
* 首頁
*/
home: home,
/**
* 一級(jí)菜單,設(shè)計(jì)包含哪些二級(jí)菜單
*/
group = [
{
id: 1,
title: '基礎(chǔ)控件',
icon: FolderOpened,
children: [
'base_html',
'base_ui',
'base_transmit',
'base_item'
]
},
{
id: 2,
title: '復(fù)合控件',
icon: FolderOpened,
children: [
'nf_form',
'nf_find',
'nf_grid',
'nf_button',
'nf_crud'
]
}
],
/**
* 二級(jí)菜單错沃,設(shè)置路徑栅组、標(biāo)題、圖標(biāo)和加載的組件
*/
routes = {
base_html: {
path: '/base-html', title: '原生HTML', icon: Edit,
component: () => import('../views/ui/base/c-01html.vue')
},
base_ui: {
path: '/base-ui', title: 'UI庫組件', icon: Edit,
component: () => import('../views//ui/base/c-02UI.vue')
},
... 略
}
})
- 在 main.js 里掛載路由和組件
import { createApp } from 'vue'
import App from './App.vue'
// 基于element-plus 二次封裝的組件
import { nfElementPlus } from '/nf-ui-elp'
// 簡(jiǎn)易路由
import router from './router'
createApp(App)
.use(nfElementPlus) // 全局注冊(cè)組件
.use(router) // 注冊(cè)路由
.mount('#app')
- 在App.vue 里面做頁面布局
<el-container>
<el-aside width="200px">
<!--菜單-->
<nf-menu/>
</el-aside>
<el-main>
<el-radio-group v-model="routerKind" size="mini">
<el-radio-button label="單頁"></el-radio-button>
<el-radio-button label="tabs"></el-radio-button>
</el-radio-group> 可以切換單頁模式枢析、動(dòng)態(tài)tab模式
<hr>
<!--路由視圖-->
<router-view v-if="routerKind === '單頁'"></router-view>
<router-view-tabs v-if="routerKind === 'tabs'"></router-view-tabs>
</el-main>
</el-container>
這里為了演示玉掸,用了兩種模式,項(xiàng)目里選擇一個(gè)喜歡的就好醒叁。
是不是很簡(jiǎn)單司浪,把繁瑣的操作都封裝好,以后再用就簡(jiǎn)單多了辐益,只需要定義路由就好断傲。
疑問
單層路由夠用嗎?
對(duì)于我來說夠用了智政,如果以后發(fā)現(xiàn)有不夠用的地方還可以繼續(xù)完善认罩,不過肯定不會(huì)完善到 VueRouter 的程度,因?yàn)槟菢拥脑捫妫瑸樯恫恢苯佑?VueRouter 垦垂?太簡(jiǎn)陋了吧宦搬?
是的,比較簡(jiǎn)陋劫拗,目前只是練練手间校,方便實(shí)現(xiàn)一些小功能的演示。