首先我們來梳理下整體上的實現思路,首先一點:整體思路和 vhr 一模一樣。
考慮到有的小夥伴可能已經忘記 vhr 中前端動態選單的實作想法了,因此本文再和大家分析一下。
為了確保在所有的 .vue
檔案中都能存取到選單數據,所以選擇將選單資料存入vuex 中,vuex 是vue 中一個儲存資料的公共地方,所有的 .vue
檔案都可以從 vuex
讀取到資料。儲存在 vuex
中的資料基本上是存在記憶體中,所以它有一個特點,就是瀏覽器按 F5 刷新之後,資料就沒了。所以在發生頁面的跳轉的時候,我們應該去區分一下,是用戶點擊了頁面上的選單按鈕之後發生了頁面跳轉還是用戶點擊了瀏覽器刷新按鈕(或者按了 F5)發生了跳轉。
為了實現這一點,我們需要用到vue 中的路由導航守衛功能,對於我們Java 工程師而言,這些可能聽起來有點陌生,但是你把它當作Java 中的Filter 來看待就好理解了,實際上我們影片中和小夥伴們講解的時候就是這麼類比的,將一個新事物跟我們腦海中一個已有的熟悉的事物進行類比,就很容易理解了。
vue 中的導航守衛就類似一個監控,它可以監控到所有的頁面跳轉,在頁面跳轉中,我們可以去判斷一下vuex 中的選單資料是否還在,如果還在,就說明使用者是點擊了頁面上的選單按鈕完成了跳轉的,如果不在,就說明使用者是點擊了瀏覽器的刷新按鈕或是按了F5 進行頁面刷新的,此時我們就要趕緊去服務端重新載入一下選單資料。
---xxxxxxxxxxxxxxxxxx---
整體上的實作想法就是這樣,接下來我們來看看一些具體的實作細節。
首先我們來看看載入的細節。
小夥伴們知道,單一頁面專案的入口是 main.js
,路由載入的內容在src/permission.js 檔案中,該檔案在main.js 中被引入, src/permission.js 中的前置導航守衛內容如下:
router.beforeEach((to, from, next) => { NProgress.start() if (getToken()) { to.meta.title && useSettingsStore().setTitle(to.meta.title) /* has token*/ if (to.path === '/login') { next({ path: '/' }) NProgress.done() } else { if (useUserStore().roles.length === 0) { isRelogin.show = true // 判断当前用户是否已拉取完user_info信息 useUserStore().getInfo().then(() => { isRelogin.show = false usePermissionStore().generateRoutes().then(accessRoutes => { // 根据roles权限生成可访问的路由表 accessRoutes.forEach(route => { if (!isHttp(route.path)) { router.addRoute(route) // 动态添加可访问路由表 } }) next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 }) }).catch(err => { useUserStore().logOut().then(() => { ElMessage.error(err) next({ path: '/' }) }) }) } else { next() } } } else { // 没有token if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 next() } else { next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页 NProgress.done() } } })
我跟大家捋一下這個前置導航守衛中的想法:
['/login', '/auth-redirect', '/bind' , '/register']。
src/store/modules/user.js 檔案中,換言之,這些使用者的基本資料載入之後,是儲存在vuex 中的,如果刷新瀏覽器這些資料就會遺失:
getInfo() { return new Promise((resolve, reject) => { getInfo().then(res => { const user = res.user const avatar = (user.avatar == "" || user.avatar == null) ? defAva : import.meta.env.VITE_APP_BASE_API + user.avatar; if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 this.roles = res.roles this.permissions = res.permissions } else { this.roles = ['ROLE_DEFAULT'] } this.name = user.userName this.avatar = avatar; resolve(res) }).catch(error => { reject(error) }) }) },方法的邏輯其實倒沒啥好說的,結合服務端回傳的JSON 格式,應該就很好理解了(部分JSON):
{ "permissions":[ "*:*:*" ], "roles":[ "admin" ], "user": "userName":"admin", "nickName":"TienChin健身", "avatar":"", } }
另外再强调下,之前在 vhr 中,我们是将请求封装成了一个 api.js 文件,里边有常用的 get、post、put 以及 delete 请求等,然后在需要使用的地方,直接去调用这些方法发送请求即可,但是在 TienChin 中,脚手架的封装是将所有的请求都提前统一封装好,在需要的时候直接调用封装好的方法,连请求地址都不用传递了(封装的时候就已经写死了),所以小伙伴们看上面的 getInfo 方法只有方法调用,没有传递路径参数等。
generateRoutes 方法则位于 src/store/modules/permission.js 文件中,这里值得说道的地方就比较多了:
generateRoutes(roles) { return new Promise(resolve => { // 向后端请求路由数据 getRouters().then(res => { const sdata = JSON.parse(JSON.stringify(res.data)) const rdata = JSON.parse(JSON.stringify(res.data)) const defaultData = JSON.parse(JSON.stringify(res.data)) const sidebarRoutes = filterAsyncRouter(sdata) const rewriteRoutes = filterAsyncRouter(rdata, false, true) const defaultRoutes = filterAsyncRouter(defaultData) const asyncRoutes = filterDynamicRoutes(dynamicRoutes) asyncRoutes.forEach(route => { router.addRoute(route) }) this.setRoutes(rewriteRoutes) this.setSidebarRouters(constantRoutes.concat(sidebarRoutes)) this.setDefaultRoutes(sidebarRoutes) this.setTopbarRoutes(defaultRoutes) resolve(rewriteRoutes) }) }) }
首先大家看到,服务端返回的动态菜单数据解析了三次,分别拿到了三个对象,这三个对象都是将来要用的,只不过使用的场景不同,下面结合页面的显示跟大家细说。
首先是调用 filterAsyncRouter 方法,这个方法的核心作用就是将服务端返回的 component 组件动态加载为一个 component 对象。不过这个方法在调用的过程中,后面还有两个参数,第二个是 lastRouter 在该方法中并无实质性作用;第三个参数则主要是说是否需要对 children 的 path 进行重写。小伙伴们知道,服务端返回的动态菜单的 path 属性都是只有一层的,例如一级菜单系统管理的 path 是 system,二级菜单用户管理的 path 则是 user,那么用户管理最终访问的 path 就是 system/path
,如果第三个参数为 true,则会进行 path 的重写,将 path 最终设置正确。
所以这里的 sidebarRoutes 和 defaultRoutes 只是能用于菜单渲染(因为这两个里边的菜单 path 不对),而最终的页面跳转要通过 rewriteRoutes 才可以实现。
除了服务端返回的动态菜单,前端本身也定义了一些基础菜单,前端的基础菜单分为两大类,分别是 constantRoutes 和 dynamicRoutes,其中 constantRoutes 是固定菜单,也就是一些跟用户权限无关的菜单,例如 404 页面、首页等;dynamicRoutes 是动态菜单,也就是也根据用户权限来决定是否展示的菜单,例如分配用户、字典数据、调度日志等等。
filterDynamicRoutes 方法则是将前端提前定义好的 dynamicRoutes 菜单进行过滤,找出那些符合当前用户权限的菜单将之添加到路由中(这些菜单都不需要在菜单栏渲染出来)。
接下来涉及到四个不同的保存路由数据的变量,分别是 routes、addRoutes(经松哥分析,这个变量并无实际作用,可以删除之)、defaultRoutes、topbarRouters 以及 sidebarRouters,四个路由变量的作用各有不同:
routes:
routes 中保存的是 constantRoutes 以及服务端返回的动态路由数据,并且这个动态路由数据中的 path 已经完成了重写,所以这个 routes 主要用在两个地方:
首页的搜索上:首页的搜索也可以按照路径去搜索,所以需要用到这个 routes,如下图:
用在 TagsView,这个地方也需要根据页面渲染不同的菜单,也是用的 routes:
sidebarRouters:
这个就是大家所熟知的侧边栏菜单了,具体展示是 constantRoutes+服务端返回的菜单,不过这些 constantRoutes 基本上 hidden 属性都是 false,渲染的时候是不会被渲染出来的。
topbarRouters:
这个是用在 TopNav 组件中,这个是将系统的一级菜单在头部显示出来的,如下图:
一级菜单在顶部显示,左边显示的都是二级三级菜单,那么顶部菜单的渲染,用的就是这个 topbarRouters。
defaultRoutes:
想要开启顶部菜单,需要在 src/layout/components/Settings/index.vue 组件中设置,如下图:
开启顶部菜单之后,点击顶部菜单,左边菜单栏会跟着切换,此时就是从 defaultRoutes 中遍历出相关的菜单设置给 sidebarRouters。
好了,這就是這四個 routes 變數的作用,老實說,腳手架中這塊的程式碼設計有點混亂,沒必要搞這麼多變量,等松哥抽空給大家優化下。
generateRoutes 方法最終會返回 rewriteRoutes 變數到前面說的那個前置導航守衛中,最終前置導航守衛將資料加入 router 中。
選單的渲染都是在 src/layout/components/Sidebar/index.vue 中完成的,看了下都是常規操作,沒啥好說的。
以上是Vue3怎麼載入動態選單的詳細內容。更多資訊請關注PHP中文網其他相關文章!