開始切入正題之前,有必要告知大家一下,這篇文章可能有一些深度,初學者可能理解會有些吃力。我會盡量把復雜問題簡單化,爭取讓每個閱讀的童鞋們都能看得懂。希望你對element-ui,vue-router,KeepAlive組件有一點了解。現在,我們開始吧。
熟悉element-ui的童鞋們都知道,ElMenuItem和ElSubMenu都需要一個index屬性,該屬性必須是唯一的。現在,我們想把路由配置直接應用于ElMenu,該如何確保index的唯一性呢?我們需要有一個生成唯一index的函數。如下是genKey函數的定義,是不是很簡單?
export const genKey = ((k = 1) => () => k++)()
現在,我們創建addRouteMetaKey函數,該函數對路由樹進行遞歸遍歷,為每一個路由配置的meta屬性動態添加key字段。這個函數很簡單,屬于最基礎的遞歸使用例子,我就不做太多解釋了。
提示:數組的forEach方法不是純函數,因為它有副作用,所以forEach方法不能稱之為函數式編程工具。
/** @param {import('vue-router').RouteRecordRaw[]} routes */
const addRouteMetaKey = routes => {
routes.forEach(route => {
if (route.meta) {
route.meta.key = genKey()
} else {
route.meta = { key: genKey() }
}
route.children && addRouteMetaKey(route.children)
})
return routes
}
通過addRouteMetaKey函數,我們可以把路由的meta.key作為index的值了。
現在,我們想實現另一個功能,就是基于標簽頁的路由組件緩存控制。使用過Vuejs KeepAlive組件的童鞋們,應該多多少少都遇到一些坑吧?在我們的項目中,有一個頂部標簽頁導航,每個tab項對應一個url,以每個url對應路由的title作為tab項標題,當切換tab的時候加載緩存中的url對應的路由組件,關閉tab項,下次打開該路由url,重新掛載對應的路由組件,而不是從緩存中加載。
當路由層級不確定的時候,KeepAlive會變的難以手動控制。所以,我劍走偏鋒,嘗試了一種簡單的解決方案,實踐證明:這是非常有效的。我的方案就是把無限層級的路由樹轉化為一維數組。通過為meta添加必要的字段,進行頁面結構個性化定制。
我們需要把每層路由配置的path轉化為全路徑,所以需要一個函數:getRouteFullPath,該函數定義如下:
/**
* 獲取路由全路徑
* @description 如果path以 / 開頭(屬于絕對路徑),則直接返回,否則拼接路徑
* @param {string} path
* @param {string} parentPath
*/
export const getRouteFullPath = (path, parentPath) => {
return !parentPath || path.startsWith('/') ? path : joinPath(parentPath, path)
}
getRouteFullPath函數中使用到的joinPath函數用于將多個路徑字符串拼接為1個完整的路徑,定義如下:
/** @param {string[]} paths */
export const joinPath = (...paths) => {
const j = '/'
const [a, ...b] = paths
return (a.endsWith('/') ? a.replace(/(/+)$/gm, j) : a + j) + b.map(_ => _.replace(/^(/+)|(/+)$/gm, '')).join(j)
}
現在,我們把路由樹轉化為一維數組。我們定義toFlatRoutes函數,該函數使用了數組的reduce方法對路由樹進行聚合遞歸,將路由配置中的path屬性的值替換為全路徑,還順便給路由配置添加了name屬性,返回一個新的一維路由配置數組。這是函數式編程和遞歸結合的一個例子。
/**
* @param {import('vue-router').RouteRecordRaw[]} routes
* @returns {import('vue-router').RouteRecordRaw[]}
*/
const toFlatRoutes = (routes, parentPath) => {
return routes.reduce((t, r) => {
const path = getRouteFullPath(r.path, parentPath)
return [
...t,
...(r.children
? toFlatRoutes(r.children, path)
: [{ ...r, path, name: r.name || `name-${genKey()}` }]
)
]
}, [])
}
以上,我們解決了KeepAlive的緩存控制問題;現在,我們又有了一個新的用戶體驗上的需求,就是我們想根據url對應的路由,自動展開該路由meta.key所屬的側邊菜單;我們通過查閱element-ui文檔得知,ElMenu有一個open方法,接收index作為參數,展開index對應的菜單。
現在的問題是,我們的路由對應的index是ElMenuItem的,而路由樹已經被我們轉化為了一維數組,通過路由的matched字段是得不到我們想要的菜單index的。所以,我們需要遍歷原始路由樹。
如下代碼,我們定義getMenuKey函數,該函數接收的參數為route對象。如果路由存在,我們進行查找。首先進行簡單查找,如果找到一個菜單menu,則返回該菜單的meta.key;如果簡單查找無果,則對路由樹進行遞歸查找;這是函數式編程和遞歸結合的另一個例子。
/**
* @param {import('vue-router').RouteRecordRaw} item
* @returns {number|undefined}
*/
export const getMenuKey = item => {
if (item) {
const menu = state.menus.find(m => item.path.startsWith(m.path))
if (menu) {
return menu.meta.key
}
const itemKey = item.meta.key
return (function findFunc (menus) {
/** @param {import('vue-router').RouteRecordRaw} m */
const fn = m => m.meta.key === itemKey ? true : m.children && m.children.some(fn)
const curMenu = menus.find(fn)
return curMenu && curMenu.meta.key
})(state.menus) // state是響應式對象,定義:const state = reactive({ menus: [] })
}
}
現在,我們大功告成了;以上就是本節的知識點,童鞋們理解了嗎?只要我們熟悉遞歸的使用,其實操作樹很簡單。如果大家還有不懂的,可以評論區問我。感謝閱讀!