# 标签页篇

# 设计细节

  1. 结构分明。为了让结构层次更明显,在tabs-head里面有子组件tabs-item,在tabs-body里面有子组件tabs-pane,表示各自的标签与内容。

    但结构分明的同时也有些许缺陷,就是tabs-itemtabs-pane都要加上相同的name属性,对于喜欢“偷懒”的程序猿来说,多写一遍属性是比较麻烦的事情。

  2. 头部支持自定义。tabs-head里有名为actions的插槽slot,可以添加按钮或其它自定义内容。

  3. 选中tab下划线滑动。使用一个div元素作为下划线容器,通过JS设置宽度和left。

# 功能细节

  1. 组件通信。因为存在爷tabs -> 父head、body -> 孙item、pane多层组件的通信以及兄弟组件的通信;为了让通信更方便,使用provideinject注入属性。

  2. 事件通信。还是上面的原因,为了让多层组件的通信更简单,这里使用了事件总线eventBus,在tabs组件通过new一个vue实例,将这个实例通过provide传递,在各组件通过inject注入,来实现组件之间的事件通信:

<script>
  // tabs.vue
  import Vue from 'vue'
  export default {
    name: "YvTabs",
    provide() {
      return {
        eventBus: this.eventBus
      }
    },
    data() {
      return {
        eventBus: new Vue()
      }
    },
    // ...
  }
</script>
  1. tab切换以及选中tab下划线滑动。使用一个div元素作为下划线容器,通过JS设置宽度和left。先看下下划线line在模板中的位置以及初始样式:
<!-- TabsHead.vue -->
<template>
  <div class="yv-tabs-head">
    <div class="title-wrapper" ref="titleWrapper">
      <slot></slot>
      <div class="line" ref='line'></div>
    </div>
    <div class="actions">
      <slot name="actions"></slot>
    </div>
  </div>
</template>

<style scoped lang="scss">
$tab-item-current-color: #296aef;
.yv-tabs-head{
  // ...
  >.title-wrapper {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    position: relative;
    >.line {
      position: absolute;
      bottom: 0;
      border-bottom: 3px solid $tab-item-current-color;
      transition: all 0.3s linear;
    }
  }
}
</style>

然后分三步走

tabs组件:在tabs组件中,通过遍历找出当前所选tab,通过eventBus传递出去:

<script>
  export default {
    // ...
    mounted() {
      // ...
      this.$children.forEach(child => {
        if (child.$options.name === 'YvTabsHead') {
          child.$children.forEach(vm => {
            if (vm.$options.name === 'YvTabsItem' && vm.name === this.selected) {
              this.eventBus.$emit('update:selected', this.selected, vm)
            }
          })
        }
      })
    }
  }
</script>

tabs-item组件:tabs-item通过eventBus监听事件接收参数,设置自身样式;点击切换时通过eventBus把name属性以及自身实例当成参数传递出去:

<script>
  export default {
    // ...
    created(){
      if (this.eventBus) {
        this.eventBus.$on('update:selected',(name,vm) => {
          this.active = name === this.name
        })
      }
    },
    methods:{
      selectTab() {
        if (this.disabled) return
        this.eventBus && this.eventBus.$emit('update:selected', this.name, this)
      }
    }
  }
</script>

tabs-head组件:通过监听eventBus,得到当前选中的tab实例,计算出当前tab元素的尺寸和位置,通过JS动态设置下划线:

<script>
  export default {
    // ...
    mounted() {
      this.eventBus.$on('update:selected',(name, vm) => {
        let line = this.$refs.line
        let { left, right, top, height } = vm.$el.getBoundingClientRect()
        let { left: wrapperLeft, top: wrapperTop, height: wrapperHeight } = this.$refs.titleWrapper.getBoundingClientRect()
        line.style.width = right - left + 'px'
        line.style.bottom = -4 +'px'
        line.style.left = left - wrapperLeft + 'px'
      })
    } 
  }
</script>

# 人工测试

手动测试。。。已完成。

# 自动化测试

test文件夹下增加tabs.test.jstabs-item.test.js文件。

因为tabs-panetabs-item基本一致,这里就不再做自动化测试了。

tabs.test.js文件有2个测试用例:测试tabs是否存在接收selected

tabs-item.test.js文件有4个测试用例:测试tabs-item是否存在接收name接收disabled点击事件

运行命令parcel watch test/* --no-cachekarma start查看测试结果:

tabs测试结果

# vuepress

docs/.vuepress/components文件夹下增加多个tabs-demo的vue文件,内容就是我们要展示的tabs示例,然后在docs/components文件夹下增加tabs的md文件,内容就是放置整个tabs组件说明。

具体内容请访问这里