vue实现Tab切换功能
在项目开发中,我们经常会碰到Tab切换的功能,而在Vue中想实现这样的功能也应该有很多种,常用的三种应该是 Tab路由切换、Tab动态组件切换、通过v-show设置Tab显示隐藏。每种方法实现起来其实都不难,看看官网介绍或看几篇博客应该就能实现。但这里面其实还有很多细节需要我们去做,如Tab切换时,切换过的Tab组件状态怎样缓存在项目中经常会有 页面A -> 页面B -> 页面...
在项目开发中,我们经常会碰到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=private
- router-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/shopping
- to.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)