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

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

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

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()"
思路
  1. 通过router配置嵌套路由
  2. 通过使用keep-alive的include属性有条件的缓存组件
  3. 通过store响应式的修改include属性对应的值
  4. 通过组件内导航钩子beforeRouteEnter、beforeRouteLeave给store提交mutations修改
实例演示

1:page1->news->page2 然后再依次返回在这里插入图片描述

通过演示我们发现

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

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

  1. 第四步跳转到第五步,为什么Tab选中为购物、内容选中为鞋包,为什么news组件及内部路由组件都缓存着
  2. 第三步返回到第二步,为什么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代码比较简单就不粘贴了

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

Logo

前往低代码交流专区

更多推荐