vue组件之Tabs标签页

4,708 阅读4分钟

预览地址 这篇博客意在总结记录Bug的解决和完成组件的过程。

Tabs标签页

<x-tabs :selected.sync="selected">
        <x-tabs-item item="经济" name="1">
         财经报道
         </x-tabs-item>
</x-tabs>

点击变换

按照上面的结构,内容应该放在tabs-item,那么选择既就由tabs控制。

tabs大致HTML结构

 <div class="tabs">
        <div class="tabs-header"........>
            <divclass="tabs-header-item".........>
                  // 标签页,这里简称  header-item
                  //.........
            </div>
            <div ref="line" class="line" v-if="!cards"></div>
            //高亮线条
        </div>
        <div class="tabs-content">
             <slot></slot>        
             //展示内容
        </div>
    </div>

由于只有两个组件,所以组件通信就非常简单了。这里不做多的说明

内容切换的动画效果

如同预览所见的(目前所有position只有一种动画效果),有一种轮播切换的赶脚。

遇到的问题

一般这种动画容易造成一些bug,例如

  • 新内容进来和旧内容出去在同一时间,会存在挤兑。官网这里由明确的一些解决方案。像是动态组件key或者mode='xxx'什么的。但是slot这种相当麻烦,在不知道插入元素的情况下,很难做到动画的顺畅和没有额外的bug。
  • 使用钩子函数都设置为绝对定位,结束改回来。但动画期间父元素失去高度。会有抖动的不美观效果。

在这里的解决方法。

首先我去Ant Design看了看。 因为这个动画效果本来就是模仿他的。

大概猜了一下,Ant Design的做法应该是把三组并排,通过控制父级的负margin控制显示。

大佬的方法还是牛批,但这里我不想多写代码就直接借鉴(chao xi)以前的轮播

只需在移出的时候绝对定位一下就行了

 position: absolute;
 left: 0;
 top: 0;

然后无非就是一个从x(左/右)移入,一个反向移出就行了。

line的位移

line的位移应该是这部分相对比较麻烦的。之所以选择用div单独一个元素来做高亮线条,而不是header-itemborder。是因为

  • 更容易变换位置,适合后面的position变换
  • 可以调整高度宽度

大概的css

首先父元素肯定要相对定位

 .tabs-header{
            display: flex;
            //....
            position: relative;
            height: $tab-height;
            //....
 }

子元素绝对定位

 >.line{
        position: absolute;
        bottom: 0;
        left: 0;
       //....
        }

根据不同的positionline完成不同方向和位置的偏移

这样子会写相当多的重复代码,非常不利于阅读和维护,例如。

代码重构:表驱动编程

先做命名上的修改

        const {line} = this.$refs
        let [left,top] = [item.offsetLeft,item.offsetTop]
        let {width,height} = item.getBoundingClientRect()
        let positionName

这里面看到其实每个位置的设置都只需要三个属性,而且topbottomleftright都是一样的设置。 所以


           //linemove函数,这名字真的不行,后面改改。
               let position = {
                   topOrBottom:{
                       width:`${width}px`,
                       height:0,
                       transform: `translate(${left}px,0)`
                   },
                   leftOrRight:{
                       width: 0,
                       height:`${height}px`,
                       transform: `translateY(${top}px)`
                   }
               }
               positionName = this.position === 'left' || this.position === 'right' ? 'leftOrRight' : 'topOrBottom';
                   line.style.height = position[positionName].height
                   line.style.width = position[positionName].width
                   line.style.transform = position[positionName].transform

这样子的修改就使代码可读性更强,一目了然。

line位置不正确的问题

其实本来是对的,展示组件的时候,position绑定动态数据,在button切换的时候,发现line位置不正确。

 watch:{
        position(){
             this.lineMove()
          }
      },

这里简单讲一下我对$nextTick的理解:

         return function queueNextTick (cb?: Function, ctx?: Object){
         let _resolve
         callbacks.push(() => {
          if (cb) {
              try {
                  cb.call(ctx)
              } catch (e) {
                 //.......
              }
                   } else if (_resolve) {
                 //...   这里是传入的回调为空且支持【promise】的时候默认选择的【promise】,这在官网里面有讲过
            }
              })
          }

先对传入的回调函数做一个收集,放进一个callbacks数组里面

事实上$nextTick也希望异步任务可以尽快一点执行。所以在api的选择上是

  • 第一步先选择属于‘微任务’promise,看看浏览器是否原生支持promise
  • 不支持则降级为macroTimerFunc
  • 同样在macroTimerFunc里根据浏览器的支持程度对这些异步api做一个选择(setImmediate,messagechannel....),反正setTimeout是最低的。

然后会在下一个tick执行(flushCallbacks)对callbacks做个从头开始的遍历执行(就像是队列一样),这里具体的比较细。反正最终就是依次执行传入的nextTick就对了。

这样子看来,在我测试这里的情况里应该是选择的promise,这当然是不行的。

因为动画过渡的影响,在执行的时候,动画也在执行,这期间元素的css属性并不是我想要的。

现在的选择就是 移除展示的动画过渡效果,其实就是展示组件,位置改变的动画没了,其他正常使用的过渡效果依然存在。

最后的代码

      
         mounted(){
            this.$nextTick(()=>{
                this.lineMove()
               //......
            })
           //.....
        }

后面会继续仿着elementAntd的组件效果,实现关于标签页更多功能。 0

博客里面如果有不正确或者错误的地方,希望大佬们可以批评指正。 如果你觉得将就还行,给我的轮子项目一个star就是最大的鼓励了。