@vue/cli3+typescript项目实战之二级导航菜单的实现及潜在的坑
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
路由下。
- import Vue from 'vue'
- import \{ RouteConfig \} from 'vue-router'
- import Home from '../views/Home.vue'
- const navList: Array<RouteConfig> = [
- {
- path: '/',
- name: 'Home',
- component: Home,
- meta: {
- title: '首页'
- }
- },
- {
- path: '/about',
- name: 'About',
- component: () => import(/* webpackChunkName: "About" */ '../views/About.vue'),
- meta: {
- title: '关于我们'
- noJump: true /* 判断父路由是否可跳转 */
- },
- /* 子路由 */
- children: [
- {
- path: '/pageA',
- name: 'PageA',
- component: () => import(/* webpackChunkName: "PageA" */ '../views/PageA.vue'),
- meta: {
- title: '页面A'
- }
- },
- {
- path: '/pageB',
- name: 'PageB',
- component: () => import(/* webpackChunkName: "PageB" */ '../views/PageB.vue'),
- meta: {
- title: '页面B'
- }
- },
- {
- path: 'pageC', /* 注意这里故意没有写上/ */
- name: 'PageC',
- component: () => import(/* webpackChunkName: "PageC" */ '../views/PageC.vue'),
- meta: {
- title: '页面C'
- }
- }
- ]
- }
- ]
- 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
复制过来。代码如下:
- <template>
- <div class="nav">
- <template v-for="(item, index) in navs">
- <!-- 无子路由时 -->
- <template v-if="!item.children">
- <router-link class="nav__link" :key="index" :to="item.path" @click.native="handleClick(item, index)">\{\{item.name\}\}</router-link>
- </template>
- <!-- 有子路由时 -->
- <template v-else>
- <div class="nav-select" :key="index" :class="\{'nav-select_active': activeIndex === index\}" @click.stop="handleClick(item, index)" @mouseenter="handleMouseenter(index)" @mouseleave="handleMouseleave">
- <!-- 点击父路由时可跳转 -->
- <template v-if="item.meta.noJump">
- <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>
- </template>
- <!-- 点击父路由时不可跳转 -->
- <template v-else>
- <a class="nav__link" :key="index">\{\{item.name\}\}</a>
- </template>
- <!-- 递归(实现无限级) -->
- <RecursiveNavigation :navs="item.children"></RecursiveNavigation>
- </div>
- </template>
- </template>
- </div>
- </template>
- <script> lang="ts">
- import { Component, Vue, Prop } from 'vue-property-decorator'
- @Component
- export default class RecursiveNavigation extends Vue {
- @Prop({ type: Array })
- navs!: any[] /* 导航数据 */
- activeIndex = -1 /* 激活的索引 */
- /* 点击事件 */
- handleClick(item: any) {
- const newNavs: any[] = this.forEachNavs(item)
- this.$emit('click-stop', newNavs)
- }
- /* 鼠标滑过 */
- handleMouseenter(index: number) {
- this.activeIndex = index
- }
- /* 鼠标离开 */
- handleMouseleave() {
- this.activeIndex = -1
- }
- /* 遍历导航,给当前的添加激活样式,其他的移除 */
- forEachNavs(item: any) {
- const newNavs: any[] = this.navs.map((ele: any) => {
- if (ele === item) {
- ele.meta.checked = true /* 当前的激活 */
- return ele
- } else {
- ele.meta.checked = false
- return ele
- }
- })
- this.activeIndex = -1
- return newNavs
- }
- }
- </script>
- <style lang="stylus" scoped>
- ...... /* css略 */
- </style>
友情提示:上面代码中的\
反斜线是为了防止在文章中转码,看官复制后替换为空即可。
3.2.2、在App.vue文件中引入组件
导航组件已经封装好了,接下来我们要在App.vue
文件中引入组件。
- <template>
- <div id="app">
- <div id="nav">
- <RecursiveNavigation> :navs="navs" @click-stop="handleClickStop"></RecursiveNavigation>
- </div>
- <router-view />
- </div>
- </template>
- <script> lang="ts">
- import \{ Component, Vue \} from 'vue-property-decorator'
- import RecursiveNavigation from '@/components/recursiveNavigation/RecursiveNavigation.vue'
- @Component({
- components: {
- RecursiveNavigation
- }
- })
- export default class App extends Vue {
- navs: any = []
- mounted() {
- this.navs = (this.$router as any).options.routes
- }
- /* 监听组件中的数据 */
- handleClickStop(newNavs: any) {
- this.navs = newNavs /* 更新导航 */
- }
- }
- </script>
友情提示:上面代码中的\
反斜线是为了防止在文章中转码,看官复制后替换为空即可。
现在,我们来看到项目在浏览器中的效果。如图:子路由中的path不带斜线时无法直接访问.gif
通过上面的gif动态图可以看get两点:
1、我们已经实现了二级导航在跳转后,父级自动添加激活样式的效果了;
2、由于pageC
页面的path
不带/
,所以无法直接访问。
如果看官细看,会发现存在一个大bug!
3.3、Bug:about下的所有子路由均未渲染对应的组件
仍然是上面的gif动态图,我们渲染pageB
、pageC
后显示的内容跟切换about
是一样的,都是:This is an about page
。显然,这是有问题的!
那要怎么解决问题呢?
3.4、解决方案:在父页面中添加router-view
要想解决问题,必须要知道事情的起因是什么。在App.vue
页面中有一个router-view
的标签,这个标签会动态根据路由不同渲染成对应的视图组件。详情请访问官方文档→→router-view
由于pageB
、pageC
的最近父级是about
,所以,我们在about
页面中添加router-view
即可。
但是,这个时候新的问题又来了。如图:父路由和子路由内容同时显示了.gif
通过gif演示可以看到,切换到about
路由时一切正常,但是到pageA
或pageB
路由时,对应的内容虽然显示了,但about
对应的内容也显示了,显然这不是我们想要的。
怎么办呢?
3.5、优化:v-if判断$route.matched
由于$route.matched
会记录路由的嵌套信息,所以我们可以根据其数组长度来控制什么时候只显示about
组件的内容,什么时候显示子路由的内容。详情请访问官网$route.matched
此处,我们先用watch
来监听路由变化(不建议这样做,因为存在bug。)。最终修改代码如下:
- <template>
- <div class="about" v-if="!isChildren">
- <h1>This is an about page</h1>
- </div>
- <!-- 有子路由时走下面 -->
- <router-view v-else></router-view>
- </template>
- <script lang="ts">
- import \{ Component, Vue, Watch \} from 'vue-property-decorator'
- @Component
- export default class About extends Vue {
- isChildren = false /* 是否有子导航 */
- @Watch('$route')
- watchRoute(to: any) {
- this.isChildren = to.matched.length > 1
- }
- }
- </script>
友情提示:上面代码中的\
反斜线是为了防止在文章中转码,看官复制后替换为空即可。
现在,我们再来点击about
下的菜单栏可以看到父、子路由对应的内容已经被区分开了。如图:使用to.matched来判断当前路由是否有嵌套路由.gif
四、总结
下面来总结下这篇文章中的要点:
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
分支中。如果看官需要研究源码,可以点击下面的链接进行访问并下载。
- 网址: 戳我前往github查看源码
转载声明:
若亲想转载本文到其它平台,请务必保留本文出处!
本文链接:/xwzj/2020-08-02/vue-cli3-recursive-navigation-2.html
若亲不想直保留地址,含蓄保留也行。艺灵不想再看到有人拿我的技术文章到他的地盘或者是其它平台做教(装)程(B)而不留下我的痕迹。文章你可以随便转载,随便修改,但请尊重艺灵的劳动成果!谢谢理解。
亲,扫个码支持一下艺灵呗~
Tag: @vue/cli3 vue项目实战 watch 路由守卫 router typescript $route.matched mouseenter 嵌套路由 ro
上一篇: 移动端页面适配之iPhoneX的安全区域 下一篇: 复盘自己在开发生鲜类小程序时犯下的几个错