vue实现Tab切换功能

mac2025-09-02  9

在项目开发中,我们经常会碰到Tab切换的功能,而在Vue中想实现这样的功能也应该有很多种,常用的三种应该是 Tab路由切换、Tab动态组件切换、通过v-show设置Tab显示隐藏。每种方法实现起来其实都不难,看看官网介绍或看几篇博客应该就能实现。

但这里面其实还有很多细节需要我们去做,如

Tab切换时,切换过的Tab组件状态怎样缓存在项目中经常会有 页面A -> 页面B -> 页面C 则从 页面C 返回 页面B 时 页面B 使用缓存数据,而页面A 跳到 页面B 时,则页面B每次都请求最新数据。比如我们在某APP内点击 最新新闻(页面A) 选项 跳转到 新闻列表(页面B) 选择某一条新闻 跳转到 新闻详情(页面C) 页面,我们希望,从新闻详情返回到新闻列表时,直接用刚才请求的数据,而不每次都重新发送请求,而从 最新新闻 跳转到 新闻列表时,则都请求最新的数据父组件如何给子组件传递参数页面内Tab来回切换后,如何直接返回到上一级页面页面循环切换时,前进或后退如何保证页面结构正确(具体下面会讲到)

Tab路由切换带缓存

想要通过路由进行切换,就需要使用嵌套路由,即整个大页面是一个路由,点击不同Tab时,再通过嵌套路由来切换不同的路由。 想要Tab切换时保存当前状态可以使用keep-alive包裹,keep-alive具体使用参考这篇文章-vue中动态添加和删除组件缓存 keep-alive 包裹Tab的组件页面我们也要动态的缓存,这里也需要用到keep-alive,只是这个keep-alive需要添加到App.vue内,各个组件的动态缓存我们使用的是keep-alive的include属性。缓存最大数使用max属性

router-link介绍

通过to属性链接目标路由,当被点击时,内部会立刻把 to 的值传到 router.push(),既然是通过router.push()的方式跳转,那么就会往history记录中添加,这样当返回时,可能就会先从Tab3返回到Tab2再返回到Tab1再返回,这种体验很不好,怎样一步返回呢,就是在router-link中添加replace属性,这样当点击时,会调用 router.replace() 而不是 router.push(),于是导航后不会留下 history 记录,这样就可一步返回了,如:<router-link :to="{ path: '/abc'}" replace></router-link>通过 命名的路由 传递参数,如:<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>通过 带查询参数 传递参数,如:<router-link :to="{ path: 'register', query: { plan: 'private' }}">Register</router-link>,结果路由为:/register?plan=privaterouter-link设置点击事件时需要添加 natvie, 如@click.native="TabClick()"

思路

通过router配置嵌套路由通过使用keep-alive的include属性有条件的缓存组件通过store响应式的修改include属性对应的值通过组件内导航钩子beforeRouteEnter、beforeRouteLeave给store提交mutations修改

实例演示

1:page1->news->page2 然后再依次返回

通过演示我们发现

从page2返回到news时,总是能返回到我们之前保存的状态从news返回到page1后,再从page1跳转news,不管news之前是什么状态,都会初始化显示购物的页面

2:page1(1)->news(2)->page2(3)->page1(4)->news(5)->page2(6) 然后再依次返回 这个视频里有几个问题需要我们去思考

第四步跳转到第五步,为什么Tab选中为购物、内容选中为鞋包,为什么news组件及内部路由组件都缓存着第三步返回到第二步,为什么Tab选中为购物、内容选中为母婴,但从右边缓存的组件看,为什么shopping组件也被缓存了

这两个问题我们后边会具体介绍

部分代码示例

1:在router中配置各个路由

这里需要注意,配置children子路由时path不能加 / ,在router-link的to后面写的路由需要以 / 开头,以 / 开头的嵌套路径会被当作根路径

export default new Router({ routes: [ { path: '/page1', name: 'page1', component: () => import(/* webpackChunkName: "test" */ './views/news/page1.vue') }, { path: '/page2', name: 'page2', component: () => import(/* webpackChunkName: "test" */ './views/news/page2.vue') }, { path: '/news', name: 'newsIndex', component: () => import(/* webpackChunkName: "test" */ './views/news/news.vue'), children: [{ path: 'sports', name: 'sports', component: () => import(/* webpackChunkName: "test" */ './views/news/sports.vue'), }, { path: 'shopping', name: 'shopping', component: () => import(/* webpackChunkName: "test" */ './views/news/shopping.vue'), }, { path: 'learn', name: 'learn', component: () => import(/* webpackChunkName: "test" */ './views/news/learn.vue'), }] } ] })
2:在App.vue组件通过computed计算属性响应式的获取store里的keepAliveArr计算属性,并赋值给keep-alive的include属性,并设置最多可缓存5个组件
<template> <div id="app"> <keep-alive :include="keepAliveArr" :max="5"> <router-view></router-view> </keep-alive> </div> </template> <script> export default { computed: { keepAliveArr() { return this.$store.getters.keepAliveArr } } }
3:在store的mutations中提供状态更改的方法,并通过store的计算属性供外部访问
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex); export default new Vuex.Store({ state: { //缓存组件数组 keepAliveArr: [] }, mutations: { UPDATE_KEEP_ALIVE(state, payload) { //当payload.type不为空则代表清除指定缓存组件,否则添加指定组件 if (payload.type) { let index = state.keepAliveArr.indexOf(payload.keepAlive); if (index !== -1) { state.keepAliveArr.splice(index, 1); //删除数组的缓存的组件 } } else { let index = state.keepAliveArr.indexOf(payload.keepAlive); if (index === -1) { state.keepAliveArr.push(payload.keepAlive); //添加需要缓存的组件 } } } }, getters: { keepAliveArr: state => state.keepAliveArr } })
4:在组件内通过导航钩子beforeRouteEnter、beforeRouteLeave给store提交mutations修改缓存组件keepAliveArr的值

这里 page1为news的上一个页面,page2为下一个页面,通过beforeRouteEnter钩子,不管从哪个页面进入都提交mutations,缓存当前news页面,当离开时判断,如果是返回上一个页面则删除当前news页面缓存,当删除news页缓存时,内部通过keep-alive保存的 购物、体育、学习三个组件缓存的状态也会一并删除,即内部的在激活的和被停用的组件都会执行销毁的生命周期。

注意:如果我们的页面比较简单,最深跳转到page2,即: page1->news->page2,然后再一级一级返回的话,那么beforeRouteEnter这两个if判断可以不写

4.1:beforeRouteEnter中两个if判断解释

4.1.1:第一个if判断

当循环跳转时,即

page1(1)->news(2)->page2(3)->page1(4)->news(5)

因为page1跳转的路径 永远是 /news/shopping,news组件又通过keep-alive保存当前状态,所以在第二步news内如果 点击了 Tab体育 或者 Tab学习时,此时currentTab不为0,但当通过 第三步->第四步->第五步 再次跳转到news 时,由于page1路径 永远是 /news/shopping,而news状态还保存在内存里不会重新 创建,此时Tab指示器显示的和下面具体内容就会不一致,所以这里判断如果是这种情况就 强制切换 到 /news/shopping 页

4.1.2: 第二个if判断

当循环跳转时又依次返回时。即

page1(1)->news(2 Tab选择 学习)->page2(3)->page1(4)->news(5 Tab选择 体育)->page2(6)

现在开始返回, 返回到第五步Tab体育时,是没有问题的,因为 news状态是缓存 的, 而第五步Tab体育页返回第四步page1时,这里 beforeRouteLeave中我们已经把news页设置不缓存 了,再继续返回到第三步page2,再返到第二步Tab学习页时,因为 最初 我们是从第二步Tab学习的路由往 下一页page2页跳的,所以这里返回也是返回到Tab学习的路由页即 /news/learn ,但因为整个news已经不缓存了,所以这里返回从第三步返回到第二步时,其实 news所有的生命周期都会执行 ,此时 currentTab的值为0,如果不通过这个判断,那么Tab指示器显示的和下面具体内容也会不一致,所以这种情况我也 强制让切换 到 /news/shopping 页

4.2:组件内导航守卫的to.path 和 to.fullPath 区别?
to.path: 是我们在router路由里定义的路由,如/news/shoppingto.fullPath: 是包括我们跳转路由时传递的参数,如/news/shopping?content=购物 beforeRouteEnter(to, from, next) { next(vm => { //添加组件缓存 vm.$store.commit("UPDATE_KEEP_ALIVE", { keepAlive: 'news' }); let path = ''; //当循环跳转时,替换路由为shopping页 if (vm.currentTab !== 0 && from.path === '/page1') { vm.currentTab = 0; path = '/news/shopping'; vm.$router.replace({ path, query: { content: '购物' } }); } //当循环跳转后,循环返回时,替换路由为shopping页 if (vm.currentTab === 0 && to.path !== '/news/shopping') { vm.currentTab = 0; path = '/news/shopping'; vm.$router.replace({ path, query: { content: '购物' } }); } }) }, beforeRouteLeave(to, from, next) { if (to.path === '/page1') {//删除缓存 this.$store.commit("UPDATE_KEEP_ALIVE", { type: 1, keepAlive: 'goods' }) } next() },

总结

通过上面四步就可以实现Tab路由切换并带组件状态缓存,这个keep-alive嵌套keep-alive需要注意的事项,大家可以参考这篇文章-vue中动态添加和删除组件缓存 keep-alive

Tab动态组件切换

大家可以参考这篇文章-vue中动态添加和删除组件缓存 keep-alive

通过v-show设置Tab显示隐藏

这个就不写了,大家只要慢慢写应该都能实现,只是用这种方式实现不太优雅。

Tab路由切换的完成代码

news代码

<template> <div class="list-container"> <div class="btn" @click="btnJumpClick">跳转到page2详情页</div> <nav class="tab-root"> <!--通过query向子路由传递参数--> <router-link v-for="(item,index) in routerList" :key="index" class="tab-button" :to="{path:item.url,query:{content:item.content}}" replace :class="{ active: currentTab === index }" @click.native="currentTab = index">{{item.tab}} </router-link> </nav> <keep-alive :include="cached" :max="3"> <router-view class="view"></router-view> </keep-alive> </div> </template> <script> export default { name: "news", data() { return { currentTab: 0, cached: 'shopping,sports,learn', about: '/news/shopping', routerList: [{ tab: '购物', url: '/news/shopping', content: '购物' }, { tab: '运动', url: '/news/sports', content: '运动' }, { tab: '学习', url: '/news/learn', content: '学习' }] } }, activated() { console.log("--news--activated--"); }, deactivated() { console.log("--news--deactivated--"); }, beforeRouteEnter(to, from, next) { next(vm => { //添加组件缓存 vm.$store.commit("UPDATE_KEEP_ALIVE", { keepAlive: 'news' }); let path = ''; //当循环跳转时,替换路由为shopping页 if (vm.currentTab !== 0 && from.path === '/page1') { vm.currentTab = 0; path = '/news/shopping'; vm.$router.replace({ path, query: { content: '购物' } }); } //当循环跳转后,循环返回时,替换路由为shopping页 if (vm.currentTab === 0 && to.path !== '/news/shopping') { vm.currentTab = 0; path = '/news/shopping'; vm.$router.replace({ path, query: { content: '购物' } }); } }) }, beforeRouteLeave(to, from, next) { if (to.path === '/page1') {//删除缓存 this.$store.commit("UPDATE_KEEP_ALIVE", { type: 1, keepAlive: 'news' }) } next() }, methods: { btnJumpClick() { this.$router.push({ path: '/page2' }) }, } } </script> <style scoped lang="scss"> .list-container { .btn { width: 100%; height: 40px; background: #f00; font-size: 20px; color: white; } .tab-root { display: flex; border-bottom: 1px solid #eee; } .tab-button { background: #fff; line-height: 40px; height: 40px; text-align: center; flex: 1; font-size: 15px; font-weight: normal; } .tab-button.active { font-size: 17px; font-weight: 500; border-bottom: 2px solid #f00; } }

shopping代码

<template> <div class="recommends-tab"> <ul class="recommends-sidebar"> <li v-for="recommend in recommends" :key="recommend.id" :class="{ selected: recommend === selectedRecommend }" @click="selectedRecommend = recommend"> {{ recommend.title }} </li> </ul> <div class="selected-recommend-container"> <div class="selected-recommend"> <div v-html="selectedRecommend.content"></div> </div> </div> </div> </template> <script> export default { name: "shopping", props:{ componentTabName:String }, data() { return { recommends: [ { id: 1, title: '母婴', content: '<p>儿童玩具、尿裤湿巾、奶粉辅食</p>' }, { id: 2, title: '鞋包', content: '<p>功能箱包、人气热卖、服饰配件</p>' }, { id: 3, title: '水果', content: '<p>瓜果桃李、海鲜水产、熟食凉菜</p>' } ], selectedRecommend: {} } }, beforeMount() { //获取通过路由传递过来的参数 console.log(this.$route.query.content); }, } </script> <style scoped lang="scss"> .recommends-tab { display: flex; } .recommends-sidebar { width: 20%; text-align: center; background: #eee; height: 100vh; } .recommends-sidebar li { height: 30px; line-height: 30px; } .recommends-sidebar li.selected { background: #fff; color: red; } .selected-recommend-container { padding-left: 10px; } </style>

page1跳转代码

btnLuYouClick() { this.$router.push({ path: '/news/shopping', query: { content: '购物' } }); }

page2跳转代码

btnJumpClick() { this.$router.push({ path: '/page1' }) },

sports和learn代码比较简单就不粘贴了

上面的代码应该已经够用,如果需要全部详细代码的就留言吧,我再单独发你。

最新回复(0)