vue 不完美的多标签页解决方案

背景

多标签页多用在中后台管理系统,能够让用户同时打开多个标签页,而且不会丢失之前填写的内容,操作起来会比较方便。虽然部分开源项目有多标签页的功能,但就体验来看,算不上特别好。

目标

  1. 可以通过router.push实现打开标签页
  2. 同一路由组件可以多开并且数据能够缓存下来
  3. 不需要处理是否缓存导致的生命周期不一致的问题
  4. 多标签页可以关闭,同时KeepAlive中的缓存清除

存在的问题

要实现多标签页的缓存,最简单的方法就是用RouterView配合KeepAlive。

<RouterView v-slot="{ Component }">   <KeepAlive>     <component :is="Component" />   </KeepAlive> </RouterView> 

然而,这个方案存在几个问题:

  1. 不能重复打开同一个路由,而是原有的组件被激活
  2. 组件生命周期发生变化

不能重复打开路由

如果给路由添加参数,打开第一次没有任何问题,但如果换另一个参数打开,还会是之前的页面,因为组件被缓存下来了。

例如:

新增一个路由 counter,在页面上添加RouterLink,并使用不同的参数

<template>   <header>     <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />     <div class="wrapper">       <HelloWorld msg="You did it!" />       <nav>         <RouterLink to="/home">Home</RouterLink>         <RouterLink to="/about">About</RouterLink>         <RouterLink to="/counter?id=1">Counter 1</RouterLink>         <RouterLink to="/counter?id=2">Counter 2</RouterLink>       </nav>     </div>   </header> <RouterView v-slot="{ Component }">   <KeepAlive>     <component :is="Component" />   </KeepAlive> </RouterView> </template> 

然后再Counter组件中获取id参数,分别点击Counter 1和Counter 2,会发现点击Counter 1时获取到的id是1,点击Counter 2时却没有任何变化,而且两个RouterLink同时是激活状态。

vue 不完美的多标签页解决方案

组件生命周期变化

和上一个问题有所关联,因为组件没有重新加载,在需要重新获取数据时,KeepAlive改变了组件的生命周期,添加了onActivatedonDeactivated生命周期。

添加一个组件测试生命周期:

<template>   <div class="about">     <h1>This is an about page</h1>   </div> </template>  <script setup>  import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, onActivated, onDeactivated } from 'vue'  onMounted(() => { console.log("onMounted") }) onUpdated(() => { console.log("onUpdated") }) onUnmounted(() => { console.log("onUnmounted") }) onBeforeMount(() => { console.log("onBeforeMount") }) onBeforeUpdate(() => { console.log("onBeforeUpdate") }) onBeforeUnmount(() => { console.log("onBeforeUnmount") }) onActivated(() => { console.log("onActivated") }) onDeactivated(() => { console.log("onDeactivated") }) </script>  <style> @media (min-width: 1024px) {   .about {     min-height: 100vh;     display: flex;     align-items: center;   } } </style>  

再修改App.vue

<template>   <header>     <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />      <div class="wrapper">       <HelloWorld msg="You did it!" />       <nav>         <RouterLink to="/home">Home</RouterLink>         <RouterLink to="/about">About</RouterLink>         <RouterLink to="/counter?id=1">Counter 1</RouterLink>         <RouterLink to="/counter?id=2">Counter 2</RouterLink>       </nav>     </div>   </header>    <RouterView v-slot="{ Component }">     <!-- <KeepAlive> -->       <component :is="Component" />     <!-- </KeepAlive> -->   </RouterView> </template>  <script setup> import { watch } from 'vue' import { RouterLink, RouterView, useRoute } from 'vue-router' import HelloWorld from './components/HelloWorld.vue'  const route = useRoute()  watch(route, () => {   console.log("页面切换", route.fullPath) })  </script> 

先从Home切换到About再切换回Home再切换回About。

查看在不使用KeepAlive切换页面时候的输出,onBeforeMount -> onMounted -> onBeforeUnmount -> onUnMounted 循环

vue 不完美的多标签页解决方案

使用KeepAlive的情况,情况就复杂很多,每次切换到页面时会激活onActivated钩子,正常情况下可以通过onActivated钩子获取路由参数,重新获取数据。

问题在于:如果组件可以在缓存与不缓存中切换,在获取数据时,需要考虑是写在onMounted里还是onActivated里,写在onMounted中时如果组件会被服用,需要处理路由参数变化重新获取数据;写在onActivated里,需要考虑组件不缓存了钩子函数不会被调用的情况。

vue 不完美的多标签页解决方案

解决方案

重复打开组件 & 生命周期变化

这个问题很好解决,只需要给KeepAlive中的component加上不同的key就可以实现,key可以通过router.fullPath来计算,这样KeepAlive中就可以缓存同一个组件多次。

<RouterView v-slot="{ Component, route }">   <KeepAlive>     <component :is="Component" :key="route.fullPath" />   </KeepAlive> </RouterView> 

vue 不完美的多标签页解决方案

同时,修改下Counter组件,查看生命周期

<template>     <div> ID = {{ id }}</div> </template>  <script setup>  import { useRoute } from 'vue-router' import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, onActivated, onDeactivated } from 'vue'  const route = useRoute() const id = route.query.id  onMounted(() => { console.log(route.fullPath, "onMounted") }) onUpdated(() => { console.log(route.fullPath, "onUpdated") }) onUnmounted(() => { console.log(route.fullPath, "onUnmounted") }) onBeforeMount(() => { console.log(route.fullPath, "onBeforeMount") }) onBeforeUpdate(() => { console.log(route.fullPath, "onBeforeUpdate") }) onBeforeUnmount(() => { console.log(route.fullPath, "onBeforeUnmount") }) onActivated(() => { console.log(route.fullPath, "onActivated") }) onDeactivated(() => { console.log(route.fullPath, "onDeactivated") }) </script> 

会发现,虽然是同一个组件,但生命周期也独立了,也就不需要考虑路由参数变化时重新获取数据,只需要在onMounted时获取一次数据就可以了。

vue 不完美的多标签页解决方案

关闭标签页

上面的问题好像一下就解决了,但第三个目标没有实现,这也是最难的一个问题。

KeepAlive可以通过给component添加不同的key达到路由多开的效果,但是却不能用key删除,KeepAlive只能通过exclude参数使用组件名称删除缓存。

这下问题麻烦了,虽然使用不同的key多开了路由,但路由的组件名称是相同的,也就是说,就算能多开了,关闭却只能全部关闭,这种是不行的。

思索后,想到了下面的方案:

不使用KeepAlive,通过监听route,变化后就向list中添加达到打开标签页的功能,渲染list中的所有组件,然后为了让组件数据缓存下来,不能使用v-if而是使用v-show来隐藏组件。

验证方案

监听route,将访问过的路由都保存下来作为打开过的标签页,当前route作为激活的标签页

编写一个TagView组件,替代RouterView+KeepAlive,关闭的时候直接删除tagView就可以

<template>   <div class="tags">     <div class="tag" v-for="tagView in tagViews" :class="{ active: tagView.key === currentTagView?.key }"      @click="router.push(tagView.route)">       {{ tagView.title }}</div>   </div>   <div class="content">     <template v-for="tagView in tagViews" :key="tagView.key">       <Component :is="tagView.component" v-show="tagView.key === currentTagView.key" />     </template>   </div> </template>  <script setup>  import { inject, ref, shallowRef, toValue, watch } from 'vue' import { useRoute, useRouter, viewDepthKey } from 'vue-router'  const route = useRoute() const router = useRouter()  const tagViews = ref([]) const currentTagView = ref(null) // 参考了vue官方的RouterView, 是RouterView嵌套的深度 const routerViewDepth = inject(viewDepthKey, 0)  const routeKey = (route) => {   return route.fullPath }  const routeTitle = (route) => {   // 还没有设计title,先用fullPath替代   return route.fullPath }  const toTagView = (route) => {   const depth = toValue(routerViewDepth)   return {     title: routeTitle(route),     key: routeKey(route),     route: { ...route },     component: shallowRef(route.matched[depth]?.components['default'])   } }  watch(route, () => {   // 判断是否已存在,存在则不添加   const key = routeKey(route)   let tagView = tagViews.value.find(tagView => tagView.key === key)   if (!tagView) {     tagView = toTagView(route)     tagViews.value.push(tagView)   }   currentTagView.value = tagView })  </script>  <style scoped> .tags {   gap: 8px;   padding: 4px;   display: flex;   border: 1px solid #ccc; }  .tag {   padding: 4px 12px;   border: 1px solid #ccc; }  .tag.active {   color: #fff;   background-color: #409EFF; } </style> 

然后在App.vue中使用

<template>   <div class="left-menu">     <RouterLink to="/counter?id=1">Counter 1</RouterLink>     <RouterLink to="/counter?id=2">Counter 2</RouterLink>   </div>   <div class="right-content">     <TagView />   </div> </template>  <script setup> import { watch } from 'vue' import TagView from './components/TagView.vue' import { RouterLink, useRoute } from 'vue-router'  const route = useRoute()  watch(route, () => {   console.log("页面切换", route.fullPath) })  </script>   <style scoped> .left-menu {   display: flex;   padding: 8px;   width: 220px;   border: 1px solid #ccc;   flex-direction: column; }  .right-content {   flex: 1;   padding: 8px; } </style>  

样式随便写的,明白意思就好。

可以自由切换标签页,并且填写的内容依然保留。

vue 不完美的多标签页解决方案

优点:编写起来很简单

缺点:之前的组件一直保留,打开的页面多了可能会卡

总结:也算一种可行的方案,但要注意页面不能太多

vue 不完美的多标签页解决方案
之前的组件只是display: none了

可能是优化

上面其实解决了最大的问题,但是还可以优化一下,可以利用KeepAlive卸载dom并缓存。

基于上面的方案,在Component外面再套一层KeepAlive,然后将v-show改成v-if。

<template>   <div class="tags">     <div class="tag" v-for="tagView in tagViews" :class="{ active: tagView.key === currentTagView?.key }"       @click="router.push(tagView.route)">       {{ tagView.title }}</div>   </div>   <div class="content">     <template v-for="tagView in tagViews" :key="tagView.key">       <KeepAlive>         <Component :is="tagView.component" v-if="tagView.key === currentTagView.key" />       </KeepAlive>     </template>   </div> </template>  

vue 不完美的多标签页解决方案

vue 不完美的多标签页解决方案

这样就解决了打开页面太多可能会导致的性能问题,但是在DevTool中就会看到很多个KeepAlive了,这也是一种取舍吧。

总结

上面的解决方案并不完美,要么容易影响性能,要么可能会影响开发(多个KeepAlive在DevTool里),要完美的话估计只能自己实现一个KeepAlive了。

发表评论

评论已关闭。

相关文章