老生常谈了!多级 虽然我们是菜单 Java 猿,但是何设写起来前端代码也不含糊!今天我想来和大家聊聊这个前端的专业动态菜单,要如何设计才显得专业!多级还是菜单以我们的 TienChin 项目为例,大家一起来看看。何设 先来一张截图看看效果: 那么这样的专业菜单是如何设计出来的呢? 今天我也不想和大家聊过多的技术细节,就聊聊这个路由是多级如何设计的,一旦大家明白了路由是菜单如何设计的,剩下的何设问题都是细枝末节的问题了。 有的专业小伙伴做过 vhr,知道 vhr 里的多级动态菜单实现方式,松哥和大家一样,菜单也是何设在不断学习不断进步中,服务器托管今天我想和大家探讨 TienChin 项目中动态菜单的实现方案,看看是否是一种更佳的解决方案。 先来和小伙伴们回顾下 vhr 中的方案: 在 vhr 中,权限的控制,只控制到二级菜单,也就是一级菜单和权限没关系。举个例子,现在有一级菜单 A 和 二级菜单 B,B 是 A 中的菜单,现在假设: 换言之,A 菜单显示与否,主要看它里边有没有子菜单需要展示,如果有,A 菜单就显示,如果没有,A 菜单就不显示。 vhr 中的思路是这样的源码下载。 在 TienChin 项目中,这一块有一些变化: 如果 A 中只有一个 B,那么似乎就没有必要再做一个两级菜单了,直接把 B 展示出来不就行了?用户操作也方便! 这是第一个不一样的地方。 基于第一点,就涉及到一个问题,就是路由接口该如何设计?最主要是接口返回的数据格式应该是什么样子的? 首先有一点小伙伴们应该知道,这里的路由是一个嵌套路由,也就是一级菜单中嵌套着二级菜单。即使这个地方在展示的时候,不存在层级关系,例如上图中的促销活动,但是底层的数据结构也应该是嵌套路由。 好啦,不卖关子了,我们来看一段路由 JSON: [{ "name": "Monitor", "path": "/monitor", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统监控", "icon": "monitor", "noCache": false, "link": null }, "children": [{ "name": "Online", "path": "online", "hidden": false, "component": "monitor/online/index", "meta": { "title": "在线用户", "icon": "online", "noCache": false, "link": null } }, { "name": "Job", "path": "job", "hidden": false, "component": "monitor/job/index", "meta": { "title": "定时任务", "icon": "job", "noCache": false, "link": null } }] }, { "path": "/", "hidden": false, "component": "Layout", "children": [{ "name": "Role", "path": "role", "hidden": false, "component": "system/role/index", "meta": { "title": "角色管理", "icon": "peoples", "noCache": false, "link": null } }] 这里我举了两个菜单的网站模板例子,这两个例子比较具有代表性,这个菜单最终显示效果大概类似下面这样: 在线用户 定时任务 大概显示效果如上图。 接下来我就来说一下这里几个典型属性: 当然,不是说你的 JSON 这么写就自动这么显示,JSON 中的东西只是一个标记,最终怎么显示,还要看渲染: v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" /> 还有一个函数我就没有列出来了,反正我们看名字也大概知道每一个函数的含义。 大家看,这个 div 中实际上分为了两部分,上面 template 专门用来处理 children 中只有一项的情况(角色管理),具体处理方式就是把 children 拿出来显示,其他的则不考虑,具体执行的时候不一定是只有一个 children,也有可能压根就没有 children,此时直接显示 parent 即可(参考 1.3 小节)。 下面的 el-submenu 则处理 children 有多个的情况(系统监控)。 另外这里涉及到了一个 resolvePath,也是特别关键的一个方法,我们来大致看下: resolvePath(routePath, routeQuery) { if (isExternal(routePath)) { return routePath } if (isExternal(this.basePath)) { return this.basePath } if (routeQuery) { let query = JSON.parse(routeQuery); return { path: path.resolve(this.basePath, routePath), query: query } } return path.resolve(this.basePath, routePath) 这个函数的主要左右,就是处理菜单的路径问题。 我们来看下这个具体的判断逻辑: 有的小伙伴可能对 path.resolve 不熟悉,我简单说下: path.resolve() 方法可以将多个路径解析为一个规范化的绝对路径,它的处理方式类似于对这些路径逐一进行 cd 操作,然而与 cd 操作不同的是,这些路径可以是文件,并且可不必实际存在(resolve() 方法不会利用底层的文件系统判断路径是否存在,而只是进行路径字符串操作)。例如: 相当于: cd foo/bar cd /tmp/file/ cd .. cd a/../subfile 举个简单的例子: path.resolve(/foo/bar, ./baz) // 输出结果为 /foo/bar/baz path.resolve(/foo/bar, /tmp/file/) // 输出结果为 /tmp/file path.resolve(wwwroot, static_files/png/, ../gif/image.gif) // 当前的工作路径是 /home/javaboy/node,则输出结果为 现在大家知道菜单跳转的路径是怎么来的了吧! 在 TienChin 项目中,菜单还存在一个外链的问题。 这个外链有两种不同的显示思路: 对于第一种情况我就不和大家演示了,对于第二种情况,我截个图给大家看下: 就是在当前项目的选项卡中,展示一个外部链接的内容。 我们先来看第一种情况。即点击菜单之后,就在一个新的选项卡中打开网页,这种菜单的 JSON 格式如下: { "name": "Http://www.javaboy.org", "path": "http://www.javaboy.org", "hidden": false, "component": "Layout", "meta": { "title": "TienChin健身官网", "icon": "guide", "noCache": false, "link": "http://www.javaboy.org" } 这个大家看,也没有 children,因为不需要,这个显示的时候,就当成了只有一个 children 来处理,然后菜单项的 path 是一个 http 路径,一点击,自然就跳到新的选项卡了。 对于第二种情况,即点击外链,在当前项目中打开一个新的选项卡,选项卡中展示链接的内容,它的 JSON 结构类似下面这样: { "name": "Http://www.javaboy.org", "path": "/", "hidden": false, "component": "Layout", "meta": { "title": "TienChin健身官网", "icon": "guide", "noCache": false, "link": null }, "children": [ { "name": "Www.javaboy.org", "path": "www.javaboy.org", "hidden": false, "component": "InnerLink", "meta": { "title": "TienChin健身官网", "icon": "guide", "noCache": false, "link": "http://www.javaboy.org" } } ] 这个其实也没啥好说的,类似于上面系统监控的那种情况,但是只有一个子菜单,在菜单渲染的时候,也是只渲染一个子菜单。由于父子菜单的 path 都不是以 http 或者 https 之类的地址开头,所以这个链接最终生成的 path 是 /www.javaboy.org,然后这个路径的内容将展示在 InnerLink 组件上,最终就是大家上图中所看到的效果了。1. 路由设计