艺灵设计

全部文章
×

@vue/cli3+typescript项目实战之二级导航菜单的实现及潜在的坑

作者:艺灵设计 - 来源:http://www.yilingsj.com - 发布时间:2020-08-02 22:37:25 - 阅: - 评:0 - 积分:0

摘要:这篇我们以实现给二级导航添加激活样式为主。通过mouseenter和mouseleave的组合使用,来实现鼠标滑过和离开时显示和隐藏子菜单的效果。通过多个添加click事件来监听用户点击操作,然后遍历整个数组并给当前对象添加checked=true属性来实现激活样式的效果。通过使用watch监听route变化来实现子路由和父路由使用不同的模板,避免组件同时渲染的bug......

在上一篇文章《@vue/cli3+typescript项目实战之给无限级嵌套的导航添加激活样式(上)》中,我们实现了一级导航菜单,接下来说二级菜单。

一、两种数据格式

1.1、children

Vue Router官方文档中,可以找到有关children实现嵌套路由的内容。详情请访问→→嵌套路由

1.2、数据同级或来自别处,根据某个特殊字段实现子路由

除了用children来控制嵌套子路由外,在一些项目中还可以让数据同级,然后再根据某个特殊的字段来动态查找相关的子路由。

二、两种dom结构

除了在数据格式上有区分外,在dom结构上同样也不止一种方案。常见的就是父子级关系,但也有非父子级关系实现的下拉菜单。常见的二级菜单有:
顶部的“个人中心”、
网页导航栏、
后台管理系统的侧边栏、
电商网站中的侧边类目栏(天猫、京东)、
......

不管使用哪种方法、哪种结构,因项目场景不同而选择不同的方案即可。在用户体验上,用户不会在意你使用了什么方法,只会在意这个功能好不好用~

三、children嵌套路由

3.1、路由改造

接着上一篇的路由,我们把/pageA/pageB/pageC放到/about路由下。

  1. import Vue from 'vue'
  2. import \{ RouteConfig \} from 'vue-router'
  3. import Home from '../views/Home.vue'
  4. const navList: Array<RouteConfig> = [
  5.   {
  6.    path: '/',
  7.     name: 'Home',
  8.     component: Home,
  9.     meta: {
  10.       title: '首页'
  11.     }
  12.   },
  13.   {
  14.     path: '/about',
  15.     name: 'About',
  16.     component: () => import(/* webpackChunkName: "About" */ '../views/About.vue'),
  17.     meta: {
  18.       title: '关于我们'
  19.       noJump: true /* 判断父路由是否可跳转 */
  20.     },
  21.     /* 子路由 */
  22.     children: [
  23.       {
  24.         path: '/pageA',
  25.         name: 'PageA',
  26.         component: () => import(/* webpackChunkName: "PageA" */ '../views/PageA.vue'),
  27.         meta: {
  28.           title: '页面A'
  29.         }
  30.       },
  31.       {
  32.         path: '/pageB',
  33.         name: 'PageB',
  34.         component: () => import(/* webpackChunkName: "PageB" */ '../views/PageB.vue'),
  35.         meta: {
  36.           title: '页面B'
  37.         }
  38.       },
  39.       {
  40.         path: 'pageC', /* 注意这里故意没有写上/ */
  41.         name: 'PageC',
  42.         component: () => import(/* webpackChunkName: "PageC" */ '../views/PageC.vue'),
  43.         meta: {
  44.           title: '页面C'
  45.         }
  46.       }
  47.     ]
  48.   }
  49. ]
  50. export default navList

友情提示:上面代码中的\反斜线是为了防止在文章中转码,看官复制后替换为空即可。

现在我们已经把路由改造完了,注意这里的子路由中的path参数中,部分都带有/,带与不带是有区别的哈!官方释义为: / 开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。如果暂时不能理解,可以继续往下看,注意下面的即将出现的gif动态图。

3.2、改造router-link

为了方便后续操作,这里我们把router-link封装成一个组件。

3.2.1、components中创建recursiveNavigation组件

在项目目录下的components中创建一个名为recursiveNavigation的文件夹,然后再创建一个名为RecursiveNavigation.vue.vue文件。接着我们把/src/App.vue中的router-link代码及相关的css复制过来。代码如下:

  1. <template>
  2.   <div class="nav">
  3.     <template v-for="(item, index) in navs">
  4.       <!-- 无子路由时 -->
  5.       <template v-if="!item.children">
  6.         <router-link class="nav__link" :key="index" :to="item.path" @click.native="handleClick(item, index)">\{\{item.name\}\}</router-link>
  7.       </template>
  8.       <!-- 有子路由时 -->
  9.       <template v-else>
  10.         <div class="nav-select" :key="index" :class="\{'nav-select_active': activeIndex === index\}" @click.stop="handleClick(item, index)" @mouseenter="handleMouseenter(index)" @mouseleave="handleMouseleave">
  11.           <!-- 点击父路由时可跳转 -->
  12.           <template v-if="item.meta.noJump">
  13.             <router-link class="nav__link" :key="index" :to="item.path" :class="\{'router-link-exact-active': item.meta.checked\}" @click.native="handleClick(item, index)">\{\{item.name\}\}</router-link>
  14.           </template>
  15.           <!-- 点击父路由时不可跳转 -->
  16.           <template v-else>
  17.             <a class="nav__link" :key="index">\{\{item.name\}\}</a>
  18.           </template>
  19.           <!-- 递归(实现无限级) -->
  20.           <RecursiveNavigation :navs="item.children"></RecursiveNavigation>
  21.         </div>
  22.       </template>
  23.     </template>
  24.   </div>
  25. </template>
  26. <script> lang="ts">
  27. import { Component, Vue, Prop } from 'vue-property-decorator'
  28. @Component
  29. export default class RecursiveNavigation extends Vue {
  30.   @Prop({ type: Array })
  31.   navs!: any[] /* 导航数据 */
  32.   activeIndex = -1 /* 激活的索引 */
  33.   /* 点击事件 */
  34.   handleClick(item: any) {
  35.     const newNavs: any[] = this.forEachNavs(item)
  36.     this.$emit('click-stop', newNavs)
  37.   }
  38.   /* 鼠标滑过 */
  39.   handleMouseenter(index: number) {
  40.     this.activeIndex = index
  41.   }
  42.   /* 鼠标离开 */
  43.   handleMouseleave() {
  44.     this.activeIndex = -1
  45.   }
  46.   /* 遍历导航,给当前的添加激活样式,其他的移除 */
  47.   forEachNavs(item: any) {
  48.     const newNavs: any[] = this.navs.map((ele: any) => {
  49.       if (ele === item) {
  50.         ele.meta.checked = true /* 当前的激活 */
  51.         return ele
  52.       } else {
  53.         ele.meta.checked = false
  54.         return ele
  55.       }
  56.     })
  57.     this.activeIndex = -1
  58.     return newNavs
  59.   }
  60. }
  61. </script>
  62. <style lang="stylus" scoped>
  63.   ...... /* css略 */
  64. </style>

友情提示:上面代码中的\反斜线是为了防止在文章中转码,看官复制后替换为空即可。

3.2.2、在App.vue文件中引入组件

导航组件已经封装好了,接下来我们要在App.vue文件中引入组件。

  1. <template>
  2.   <div id="app">
  3.     <div id="nav">
  4.       <RecursiveNavigation> :navs="navs" @click-stop="handleClickStop"></RecursiveNavigation>
  5.     </div>
  6.     <router-view />
  7.   </div>
  8. </template>
  9. <script> lang="ts">
  10. import \{ Component, Vue \} from 'vue-property-decorator'
  11. import RecursiveNavigation from '@/components/recursiveNavigation/RecursiveNavigation.vue'
  12. @Component({
  13.   components: {
  14.     RecursiveNavigation
  15.   }
  16. })
  17. export default class App extends Vue {
  18.   navs: any = []
  19.   mounted() {
  20.     this.navs = (this.$router as any).options.routes
  21.   }
  22.   /* 监听组件中的数据 */
  23.   handleClickStop(newNavs: any) {
  24.     this.navs = newNavs /* 更新导航 */
  25.   }
  26. }
  27. </script>

友情提示:上面代码中的\反斜线是为了防止在文章中转码,看官复制后替换为空即可。

现在,我们来看到项目在浏览器中的效果。如图:子路由中的path不带斜线时无法直接访问.gif子路由中的path不带斜线时无法直接访问

通过上面的gif动态图可以看get两点:
1、我们已经实现了二级导航在跳转后,父级自动添加激活样式的效果了;
2、由于pageC页面的path不带/,所以无法直接访问。

如果看官细看,会发现存在一个大bug

3.3、Bug:about下的所有子路由均未渲染对应的组件

仍然是上面的gif动态图,我们渲染pageBpageC后显示的内容跟切换about是一样的,都是:This is an about page。显然,这是有问题的!

那要怎么解决问题呢?

3.4、解决方案:在父页面中添加router-view

要想解决问题,必须要知道事情的起因是什么。在App.vue页面中有一个router-view的标签,这个标签会动态根据路由不同渲染成对应的视图组件。详情请访问官方文档→→router-view
由于pageBpageC的最近父级是about,所以,我们在about页面中添加router-view即可。

但是,这个时候新的问题又来了。如图:父路由和子路由内容同时显示了.gif父路由和子路由内容同时显示了

通过gif演示可以看到,切换到about路由时一切正常,但是到pageApageB路由时,对应的内容虽然显示了,但about对应的内容也显示了,显然这不是我们想要的。

怎么办呢?

3.5、优化:v-if判断$route.matched

由于$route.matched会记录路由的嵌套信息,所以我们可以根据其数组长度来控制什么时候只显示about组件的内容,什么时候显示子路由的内容。详情请访问官网$route.matched
此处,我们先用watch来监听路由变化(不建议这样做,因为存在bug。)。最终修改代码如下:

  1. <template>
  2.   <div class="about" v-if="!isChildren">
  3.   <h1>This is an about page</h1>
  4.   </div>
  5.   <!-- 有子路由时走下面 -->
  6.   <router-view v-else></router-view>
  7. </template>
  8. <script lang="ts">
  9. import \{ Component, Vue, Watch \} from 'vue-property-decorator'
  10. @Component
  11. export default class About extends Vue {
  12.   isChildren = false /* 是否有子导航 */
  13. @Watch('$route')
  14.   watchRoute(to: any) {
  15.   this.isChildren = to.matched.length > 1
  16.   }
  17. }
  18. </script>

友情提示:上面代码中的\反斜线是为了防止在文章中转码,看官复制后替换为空即可。

现在,我们再来点击about下的菜单栏可以看到父、子路由对应的内容已经被区分开了。如图:使用to.matched来判断当前路由是否有嵌套路由.gif使用to.matched来判断当前路由是否有嵌套路由

四、总结

下面来总结下这篇文章中的要点:
1、如何给父级添加激活样式?
答:添加click事件,注意是多处添加。当用户点击时,循环整个导航数组,给当前数组添加checked=true的属性,非当前路由设置为checked=false,主要是避免同时出现多个高亮。除此之外,也可以通过ref的方式操作dom
2、滑过或离开导航菜单时,如何控制子菜单的显示与隐藏?
答:先定义一个activeIndex变量,滑过mouseenter注意与mouseover的区别)时动态修改activeIndex的值为当前索引,然后通过动态增删nav-select_active类名实现子菜单的显示与隐藏。为了避免在同级切换时activeIndex存在默认值干扰,在离开mouseleave时重置其值为负,这样确保不会添加激活类名nav-select_active
3、如何控制路由对应视图组件?
答:先定义一个布尔类型的变量isChildren,利用watch监听路由的matched属性,然后动态改变isChildren的值。
4、子路由的path不带/时如何访问?
答:视情况添加父路由的路径即可。如:/about/pageC

至此,我们终于实现了二级导航切换时给父级菜单添加激活样式的效果了,但这并不代表我们的代码是健壮的,因为还存在很多不完美的地方。比如:
在嵌套路由页面刷新页面后,父级的激活样式丢失了;
在嵌套路由页面刷新页面,对应的视图组件模板丢失了;
三级以上嵌套路由时,父级上的激活样式失效;

除此之外,我们可不可以通过不监听click事件来实现想要的功能呢?看官可以先思考下。

关于以上问题,艺灵会在下一篇中给出个人的见解。

五、demo源码

本节的源码已上传到了github上的dev-recursive-navigation-2-20200802分支中。如果看官需要研究源码,可以点击下面的链接进行访问并下载。

  1. 网址: 戳我前往github查看源码
转载声明:
  若亲想转载本文到其它平台,请务必保留本文出处!
本文链接:/xwzj/2020-08-02/vue-cli3-recursive-navigation-2.html

若亲不想直保留地址,含蓄保留也行。艺灵不想再看到有人拿我的技术文章到他的地盘或者是其它平台做教(装)程(B)而不留下我的痕迹。文章你可以随便转载,随便修改,但请尊重艺灵的劳动成果!谢谢理解。

亲,扫个码支持一下艺灵呗~
如果您觉得本文的内容对您有所帮助,您可以用支付宝打赏下艺灵哦!

Tag: @vue/cli3 vue项目实战 watch 路由守卫 router typescript $route.matched mouseenter 嵌套路由 ro

上一篇: 移动端页面适配之iPhoneX的安全区域   下一篇: 复盘自己在开发生鲜类小程序时犯下的几个错

评论区