動態加載菜單
之前我們的導航樹都是寫死在頁面里的,而實際應用中是需要從后台服務器獲取菜單數據之后動態生成的。
我們在這里就用上一篇准備好的數據格式Mock出模擬數據,然后動態生成我們的導航菜單。
接口模塊化
我們向來講究模塊化,之前接口都集中在,interface.js,我們現在把它改名為 api.js,並把里邊原來登錄、用戶、菜單的相關接口都轉移到我們新建的接口模塊文件中。
模塊化之后的文件結構如下圖所示

模塊化之后,模塊接口寫在相應的模塊接口文件中,如下面是登錄模塊
login.js
import axios from '../axios'
/*
* 系統登錄模塊
*/
// 登錄
export const login = data => {
return axios({
url: '/login',
method: 'post',
data
})
}
// 登出
export const logout = () => {
return axios({
url: '/logout',
method: 'get'
})
}
模塊化之后,父模塊可以像這樣引入
api.js
/*
* 接口統一集成模塊
*/
import * as login from './moudules/login'
import * as user from './moudules/user'
import * as menu from './moudules/menu'
// 默認全部導出
export default {
login,
user,
menu
}
因為我們這里是導出的是父模塊,所以在具體接口調用的時候,也需要在原來的基礎上加上模塊了,像這樣。
如上面 api.js 中,我們導出了 login 的整個文件,而 login 文件下有 login,logout 等多個方法。

導航菜單樹接口
我們在 menu.js 下創建一個查詢導航菜單樹的接口。
import axios from '../axios'
/*
* 菜單管理模塊
*/
export const findMenuTree = () => {
return axios({
url: '/menu/findTree',
method: 'get'
})
}
api.js 中如果沒引入要記得引入。

頁面接口調用
接口已經有了,我們在導航菜單組件 MenuBar.vue 中,加載菜單並存入 store 。

頁面菜單渲染
還是在 MenuBar.vue 中,頁面通過封裝的菜單樹組件讀取store數據,遞歸生成菜單。

新建菜單樹組件,遞歸生成菜單,並在點擊響應函數里面根據菜單URL跳轉到指定路由。
components/MenuTree/index.js
<template>
<el-submenu v-if="menu.children && menu.children.length >= 1" :index="menu.menuId + ''">
<template slot="title">
<i :class="menu.icon"></i>
<span slot="title">{{menu.name}}</span>
</template>
<MenuTree v-for="item in menu.children" :key="item.menuId" :menu="item"></MenuTree>
</el-submenu>
<el-menu-item v-else :index="menu.menuId + ''" @click="handleRoute(menu)">
<i :class="menu.icon"></i>
<span slot="title">{{menu.name}}</span>
</el-menu-item>
</template>
<script>
export default {
name: 'MenuTree',
props: {
menu: {
type: Object,
required: true
}
},
methods: {
handleRoute (menu) {
// 通過菜單URL跳轉至指定路由
this.$router.push(menu.url)
}
}
}
</script>
提供Mock數據
接口有了,頁面調用和渲染也寫好了,該提供Mock數據了。
mock/modules/menu.js 中 mock findTree接口,data 對應數據太多,這里不貼了。
export function findTree() {
return {
url: 'http://localhost:8080/menu/findTree',
type: 'get',
data: menuTreeData // json 對象數據
}
}
測試效果
啟動完成,進入主頁,我們看到導航菜單已經成功加載進來了,oh yeah!

然而,我們愉悅的點了點菜單,發現是這樣的情況,oh no !

毛都沒有,不過顯然,聰明的你已經看穿了一切,我們之前只提供了一個叫 /user 的路由,並沒有提供 /sys/user 的路由。
好吧,我們稍微修改一下,打開路由配置,把 /user 改成 /sys/user 試試。

果不其然,修改完之后便可以正常跳轉到用戶界面了。

但不對呀,這里路由配置是寫死的,導航菜單是菜單數據動態生成的,這個路由配置也應該是根據菜單數據動態添加的啊,嗯,所以接下來我們就來討論動態路由配置的問題。
動態路由實現
在 vue 的 route 中提供了 addRoutes 來實現動態路由,打開 MenuBar.vue ,我們在加載導航菜單的同時添加動態路由配置。
MenuBar.vue

其中 addDynamicMenuRoutes 是根據菜單返回動態路由配置的關鍵代碼。
addDynamicMenuRoutes 方法詳情:
/**
* 添加動態(菜單)路由
* @param {*} menuList 菜單列表
* @param {*} routes 遞歸創建的動態(菜單)路由
*/
addDynamicMenuRoutes (menuList = [], routes = []) {
var temp = []
for (var i = 0; i < menuList.length; i++) {
if (menuList[i].children && menuList[i].children.length >= 1) {
temp = temp.concat(menuList[i].children)
} else if (menuList[i].url && /\S/.test(menuList[i].url)) {
menuList[i].url = menuList[i].url.replace(/^\//, '')
// 創建路由配置
var route = {
path: menuList[i].url,
component: null,
name: menuList[i].name,
meta: {
menuId: menuList[i].menuId,
title: menuList[i].name,
isDynamic: true,
isTab: true,
iframeUrl: ''
}
}
// url以http[s]://開頭, 通過iframe展示
if (isURL(menuList[i].url)) {
route['path'] = menuList[i].url
route['name'] = menuList[i].name
route['meta']['iframeUrl'] = menuList[i].url
} else {
try {
// 根據菜單URL動態加載vue組件,這里要求vue組件須按照url路徑存儲
// 如url="sys/user",則組件路徑應是"@/views/sys/user.vue",否則組件加載不到
let array = menuList[i].url.split('/')
let url = array[0].substring(0,1).toUpperCase()+array[0].substring(1) + '/' + array[1].substring(0,1).toUpperCase()+array[1] .substring(1)
route['component'] = resolve => require([`@/views/${url}`], resolve)
} catch (e) {}
}
routes.push(route)
}
}
if (temp.length >= 1) {
this.addDynamicMenuRoutes(temp, routes)
} else {
console.log(routes)
}
return routes
}
動態菜單頁面的組件結構稍微調整下,需要跟菜單url匹配,才能根據菜單url確定組件路徑來動態加載組件。

把路由文件清理一下,把動態菜單相關的路由配置處理掉,留下一些固定的全局路由就好。

動態路由測試
啟動完成,進入主頁,點擊用戶管理,路由到了用戶管理頁面。

點擊機構管理,路由到了機構管理頁面。

好了,到這里動態路由功能已經實現了,給自己鼓個掌吧。
頁面刷新出大坑
先前我們是將導航菜單和路由的加載放在菜單欄頁面MenuBar.vue中,一切顯示和路由也都正常,看起來沒什么問題。然而當我們在非根據路徑刷新頁面時,問題出現了。
如下圖所示,我們在用戶管理頁面的時候,點擊刷新瀏覽器,然后就白茫茫一片了,這是因為瀏覽器的刷新會導致整個vue重新加載,路由被重新初始化了,后面在Menu.bar添加的動態路由沒有了,所以跳轉的時候沒有找到匹配路由,跳轉的是一個不存在的頁面,故而白茫茫一片。

專業填坑指南
這顯然是動態菜單和路由的加載時機不對,怎么解決這個問題呢,既然問題出在加載時機,那就找一個在頁面屬性的時候也能觸發重新加載的地方就好了。
這樣的地方也不少,像vue加載過程中的鈎子函數,路由導航守衛函數等都可以,我們這里就選擇在路由導航守衛的 beforeEach 函數內加載,保證每次路由跳轉的時候都能夠擁有動態菜單和路由。
把原先在MenuBar.vue中加載動態菜單和路由的代碼,轉移到路由配置 router/index 中來。
beforeEach:
router.beforeEach((to, from, next) => {
// 登錄界面登錄成功之后,會把用戶信息保存在會話
// 存在時間為會話生命周期,頁面關閉即失效。
let isLogin = sessionStorage.getItem('user')
if (to.path === '/login') {
// 如果是訪問登錄界面,如果用戶會話信息存在,代表已登錄過,跳轉到主頁
if(isLogin) {
next({ path: '/' })
} else {
next()
}
} else {
// 如果訪問非登錄界面,且戶會話信息不存在,代表未登錄,則跳轉到登錄界面
if (!isLogin) {
next({ path: '/login' })
} else {
// 加載動態菜單和路由
addDynamicMenuAndRoutes()
next()
}
}
})
/**
* 加載動態菜單和路由
*/
function addDynamicMenuAndRoutes() {
api.menu.findMenuTree()
.then( (res) => {
store.commit('setMenuTree', res.data)
// 添加動態路由
let dynamicRoutes = addDynamicRoutes(res.data)
router.options.routes[0].children = router.options.routes[0].children.concat(dynamicRoutes)
router.addRoutes(router.options.routes);
})
.catch(function(res) {
alert(res);
});
}
/**
* 添加動態(菜單)路由
* @param {*} menuList 菜單列表
* @param {*} routes 遞歸創建的動態(菜單)路由
*/
function addDynamicRoutes (menuList = [], routes = []) {
var temp = []
for (var i = 0; i < menuList.length; i++) {
if (menuList[i].children && menuList[i].children.length >= 1) {
temp = temp.concat(menuList[i].children)
} else if (menuList[i].url && /\S/.test(menuList[i].url)) {
menuList[i].url = menuList[i].url.replace(/^\//, '')
// 創建路由配置
var route = {
path: menuList[i].url,
component: null,
name: menuList[i].name,
meta: {
menuId: menuList[i].menuId,
title: menuList[i].name,
isDynamic: true,
isTab: true,
iframeUrl: ''
}
}
// url以http[s]://開頭, 通過iframe展示
if (isURL(menuList[i].url)) {
route['path'] = menuList[i].url
route['name'] = menuList[i].name
route['meta']['iframeUrl'] = menuList[i].url
} else {
try {
// 根據菜單URL動態加載vue組件,這里要求vue組件須按照url路徑存儲
// 如url="sys/user",則組件路徑應是"@/views/sys/user.vue",否則組件加載不到
let array = menuList[i].url.split('/')
let url = array[0].substring(0,1).toUpperCase()+array[0].substring(1) + '/' + array[1].substring(0,1).toUpperCase()+array[1] .substring(1)
route['component'] = resolve => require([`@/views/${url}`], resolve)
} catch (e) {}
}
routes.push(route)
}
}
if (temp.length >= 1) {
addDynamicRoutes(temp, routes)
} else {
console.log(routes)
}
return routes
}
當然,別忘了把要用到的幾個東西引入進來,把導航菜單欄的代碼清理一下。

測試效果
啟動完成,進入主頁,點擊用戶管理,點擊刷新按鈕。

刷新后,菜單收起來了,然而頁面還是正確的停留在用戶管理頁面。媽媽再也不用擔心我會刷新了!

保存加載狀態
現在每次路由跳轉前都會重新獲取菜單數據生成菜單和路由,及時頁面沒有刷新也會重復獲取,這樣很影響性能。我們改良一下,加載成功之后把狀態保存到store,每次加載之前先檢查store的加載狀態,這樣就可以避免在非頁面刷新的情形下還頻發重復的加載了。
在 store 中添加菜單路由加載狀態,避免頁面未刷新而重復加載。

修改路由配置,在加載之前判斷加載狀態,只有未加載的情況下才加載,並在加載之后保存加載狀態。

求解一個問題
在路由跳轉的時候,路由好像是在原路徑基礎上疊加路由路徑跳轉的。
如路徑在 http://localhost:8090/#/sys/dept 的時候,點擊用戶管理。
代碼對應 this.$router.push(‘’sys/user),路由就賺到了 http://localhost:8090/#/sys/sys/user。
比正確路由多了一個 sys,目前還不到為什么。

目前我是在實際跳轉之前,先跳回主頁面然后在做真正的跳轉。
這樣問題可以解決,但無端端多了一步跳轉總歸不好,求解中。。。


