Vue多标签页应用解决方案

6,479 阅读7分钟

业务场景

应用系统中需要运用到多标签页,跟浏览器一样的效果,在新打开页面后,动态追加一个页签,点击页签可以切换系统主页面区域的内容,且保持内容不刷新,如果关闭页签再通过菜单打开,重新加载。jQuery年代的解决方案就是ifream,但是在Vue.js的单页面应用中,都是组件化开发了,实现多标签页解决的问题就聚焦在关于组件实例的缓存和销毁,下边讲述两种实现多标签页系统的技术实现方案。

Vue Router方式

Vue Router是官方的路由管理器,跟Vue.js深度集成,确实很强,更多的功能不意义阐述,详情请参考Vue Router 官方文档

本文重点说明,使用Vue Router实现多标签页可能会遇到的问题,以及相应的解决方案。

当我们跳转一个新的路由时,可能是点击菜单,也可能是点击发生跳转,这时候路由会发生切换,我们都希望打开一个新的页签,并且这个页签并处于激活状态,当打开多个时,可以切换页签的激活状态,同时路由组件区域(页面内容)发生变化,始终展示激活页签对应的内容。

实现思路

一个组件展示所有的标签页,并标识出来哪个是激活状态,标签页数据需要在菜单等地方实现追加,我们优先选择数据存在Vuex中,由标签页组件实现切换激活标签和删除标签,添加标签页也可以在该组件内实现,需要对路由进行监听,路由发生调用Vuex中的增加函数,函数对路由数据进行过滤,这样就可以实现路由变化时,标签页数据的变化。

在 外层包裹 具体原理参考 在动态组件上使用 keep-alive

这个时候,在你切换渲染不同路由的时候,确实好使,组件的状态数据、表单填写的内容等也会被缓存,好像这么简单都直接解决了,当然不可能,这么简单就没必要写这篇文章。

当你更改路由参数的时候,你会发现参数变化的时候,不太好使了,会把之前缓存的这个组件销毁重新加载,一个组件只能被缓存一个,我们希望的肯定是参数不一样时,应该分开都缓存,比如一些明细页面,我们需要看多个客户、多个工单,不应该每次切换都是在重新加载。

  • 问题1: 更换路由参数时,不会同时缓存
  • 解决方案1:router-view组件中加入key,保持key的唯一,如fullPath,这个时候当你$route.fullPath不一致时,都会被缓存
     <keep-alive>
          <router-view :key="$route.fullPath" />
     </keep-alive>

关闭页签时,我们更换路由,实现跳转,当再次访问这个路由时,组件未重新加载,这个就不符合正常的使用习惯,关闭页签就应该啥都没了,所以缓存啥的应该都没了。

  • 问题2: 关闭页签(路由跳转)后,再次进入改路由时,组件未重新加载,也就是组件未卸载,读取了缓存。
  • 解决方案2: 这个问题的解决方案有点复杂,分步骤说明解决:
  1. 当页签关闭时,我们需要销毁该路由对应的组件,keep-alive 组件停用时调用destroy,更多说明可参考Vue.js API中 destroy用法deactivated用法 在组件生命周期钩子函数(deactivated)中,调用this.$destroy(),当keep-alive内的组件停用时销毁该组件,如下
   deactivated() {
         this.$destroy();
   }
  1. 上边的写法可以解决路由跳转时销毁组件,但是新的问题是,在切换页签时也会销毁,还会将相同路由不同参数的组件全部卸载,切换页签时我们是不希望被销毁的,因为销毁预示再次打开要重新加载。所以在执行this.$destroy()的时候要进行判断,判断Vuex的当前打开页签数据,来判断该组件是否要销毁。

  2. 如何判断该组件是否销毁,你会说当然是去判断当前路由是否在页签数据里,问题是你怎么判断?那什么区判断? 我采用的解决方案是让组件接收router-view声明的key,接收的方法是this.$vnode.key,这个key是唯一的,所以我们全局存储的页签数据也用这个key,名字暂且叫visitedViews。这样就有了一对一的关系,visitedViews中每一个元素(页签)的key和router-viewkey是同一个值,在组件内将拿this.$vnode.key去打开的页签数据(visitedViews)中判断是否存在,当不存在时卸载该组件,这个时候不会影响到同路由不同参数的组件。

组件有没有缓存可以通过DevTools调试查看

DevTools调试效果图

这样看起来,好像才是真的解决了所有的问题,但是还是有bug,有瑕疵,解决方案2中提到的一大堆过程的前提是destroy,只有组件进入destroy才满足解决方案2的方法,但是如果页签没有激活,keep-alive中就已经是停用了这个组件,关闭页签时,路由是没有变化的,keep-alive对各个组件也并没有发生启用和停用,只是改变了Vuex中visitedViews的数据,所以关键问题是只要visitedViews发生变化,需要把减少的那个组件给卸载掉。

  • 问题3: 当关闭非激活页签时,关闭页签对应的组件无法卸载。
  • 解决方案3 前边我们已经得出结论,组件的卸载应该有Vuex中visitedViews来决定,所以放弃destroy,改用监听visitedViews,执行的方法跟destroy中执行的一样,也是取this.$vnode.keyvisitedViews判断,没有就执行this.$destroy()卸载。

至此,卸载这个活儿终于完成了,无论是怎么关闭页签,都可以完成组件的卸载,切换页签不卸载。感觉完美无瑕,可现实总是那么残酷,关闭页签后,再打开,切换页签时,这个组件居然刷新了,明明关闭前,切换是不刷新的,关闭再打开后,就会刷新了,难道不会缓存了?看一下DevTools调试,懵了。。。

DevTools调试效果图

  • 问题4 当组件卸载后,无法缓存
  • 解决方案
  1. this.$destroy()是有点问题的,卸载完组件后,对于该组件已经无法缓存,官方是建议在大多数场景中你不应该调用这个方法。最好使用 v-if 和 v-for 指令以数据驱动的方式控制子组件的生命周期。
  2. 可以尝试在router-view加入v-if,v-if判断当前的$route.fullPath是否存在visitedViews,发现没有任何用处
  3. v-if能绑定一个固定的属性值,比如在路由元中声明一个该路由对应组件的缓存状态,v-if写这个状态,当visitedViews变化时,修改和这个状态,从而控制改组件卸载,问题就是带参数怎么办,main?id=1main?id=2,要缓存main?id=2不缓存main?id=1无法解决
  4. include,这是官方给出允许组件有条件地缓存的方案。注意该匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。问题跟上边一样,只能到组件,不能识别参数,分开缓存和卸载。
  5. 到这儿,感觉已经怀疑人生了,所有的路已经全部堵死,是不是 Vue Router方式无法实现组件带参数的多标签页。
  6. 决定通过include卸载,监听开打的组件名,通过include控制组件是否缓存。天真的认为main?id=1main?id=2都打开时,必须全部关闭时main才会被全部卸载。
  • 问题5 include控制组件卸载时,只卸载了最后一个(如:main?id=1main?id=2,只卸载了main?id=2main?id=1无法卸载) 解决方案 1.就是一个带参数的组件缓存与卸载,怎么这么费劲,尝试将问题3和问题4的解决方案进行了混合使用,在组件内加入监听,监听页签中没有该组件的this.$vnode.key,执行this.$destroy() 2.终终终终终...于 搞定了,可以带参数分开卸载,而且还能再次缓存。

实现方案

<keep-alive> 添加include,Vuex获取当前打开页签的组件名(如:visitedName),include=visitedName;针对传参数的页面,添加watch,监听Vuex中标签页的变化,当有变化时判断是否包含当前组件,没有时卸载当前组件,当组件不需要传参时可以不添加watch,include不缓存时组件将会被卸载。

watch: {
    visitedFullPath: function(newval, oldval) {
      if (this.searchTag(this.$vnode.key) < 0) {//判断当前页面是否关闭
        this.$destroy();
      }
    }
  }

注意: 页面组件必须写name,声明组件名称