这样我们就拥有了一个支持多级展开、子菜单分别对应页面路由的侧边栏菜单。细心的朋友可能还发现了,虽然父菜单并不对应一个具体的路由但在配置项中依然还有 path 这个属性,这是为什么呢?
2、处理菜单高亮
在传统的企业管理系统中,为不同的页面配置页面路径是一件非常痛苦的事情,对于页面路径,许多开发者唯一的要求就是不重复即可,如上面的例子中,我们把菜单数据配置成这样也是可以的。
const menuData = [{ name: '仪表盘', icon: 'dashboard', children: [{ name: '分析页', children: [{ name: '实时数据', path: '/realtime', }, { name: '离线数据', path: '/offline', }], }], }]; <Router> <Route path="/realtime" render={() => <div />} <Route path="/offline" render={() => <div />} </Router>
用户在点击菜单项时一样可以正确地跳转到相应页面。但这样做的一个致命缺陷就是,对于 /realtime 这样一个路由,如果只根据当前的 pathname 去匹配菜单项中 path 属性的话,要怎样才能同时也匹配到「分析页」与「仪表盘」呢?因为如果匹配不到的话,「分析页」和「仪表盘」就不会被高亮了。我们能不能在页面的路径中直接体现出菜单项之间的继承关系呢?来看下面这个工具函数。
import map from 'lodash/map'; const formatMenuPath = (data, parentPath = 'https://www.jb51.net/') => ( map(data, (item) => { const result = { ...item, path: `${parentPath}${item.path}`, }; if (item.children) { result.children = formatMenuPath(item.children, `${parentPath}${item.path}/`); } return result; }) );
这个工具函数把菜单项中可能有的 children 字段考虑了进去,将一开始的菜单数据传入就可以得到如下完整的菜单数据。
[{ name: '仪表盘', icon: 'dashboard', path: '/dashboard', // before is 'dashboard' children: [{ name: '分析页', path: '/dashboard/analysis', // before is 'analysis' children: [{ name: '实时数据', path: '/dashboard/analysis/realtime', // before is 'realtime' }, { name: '离线数据', path: '/dashboard/analysis/offline', // before is 'offline' }], }], }];
然后让我们再对当前页面的路由做一下逆向推导,即假设当前页面的路由为 /dashboard/analysis/realtime ,我们希望可以同时匹配到 ['/dashboard', '/dashboard/analysis', '/dashboard/analysis/realtime'] ,方法如下:
import map from 'lodash/map'; const urlToList = (url) => { if (url) { const urlList = url.split('https://www.jb51.net/').filter(i => i); return map(urlList, (urlItem, index) => `/${urlList.slice(0, index + 1).join('https://www.jb51.net/')}`); } return []; };
上面的这个数组代表着不同级别的菜单项,将这三个值分别与菜单数据中的 path 属性进行匹配就可以一次性地匹配到所有当前页面应当被高亮的菜单项了。
这里需要注意的是,虽然菜单项中的 path 一般都是普通字符串,但有些特殊的路由也可能是正则的形式,如 /outlets/:id 。所以我们在对二者进行匹配时,还需要引入 path-to-regexp 这个库来处理类似 /outlets/1 和 /outlets/:id 这样的路径。又因为初始时菜单数据是树形结构的,不利于进行 path 属性的匹配,所以我们还需要先将树形结构的菜单数据扁平化,然后再传入 getMeunMatchKeys 中。
import pathToRegexp from 'path-to-regexp'; import reduce from 'lodash/reduce'; import filter from 'lodash/filter'; const getFlatMenuKeys = menuData => ( reduce(menuData, (keys, item) => { keys.push(item.path); if (item.children) { return keys.concat(getFlatMenuKeys(item.children)); } return keys; }, []) ); const getMeunMatchKeys = (flatMenuKeys, paths) => reduce(paths, (matchKeys, path) => ( matchKeys.concat(filter(flatMenuKeys, item => pathToRegexp(item).test(path))) ), []);
在这些工具函数的帮助下,多级菜单的高亮也不再是问题了。
3、知识点:记忆化(Memoization)
在侧边栏菜单中,有两个重要的状态:一个是 selectedKeys ,即当前选定的菜单项;另一个是 openKeys ,即多个多级菜单的打开状态。这二者的含义是不同的,因为在 selectedKeys 不变的情况下,用户在打开或关闭其他多级菜单后, openKeys 是会发生变化的,如下面二图所示, selectedKeys 相同但 openKeys 不同。