VueJS

流程图与代码:一个Tabs组件的封装过程

2018-12-08  本文已影响37人  凌霄光

需求描述

最近在做这样一个需求:在商品列表之上加一个分类,点击分类可以切换商品。

交互分析

商品列表已经有了,需要做的只是上面的分类的tab,我画了一下整个页面的交互图:


tap和pullDown的时候都会重新请求数据,并且重新渲染,过程中涉及骨架图和loading的显示隐藏。

已有的组件

我看了下已有的基础组件,发现有一个能部分满足我的需求,但是又有一些需要修改的地方:样式有很多不同,tab过多时不支持滚动显示,而且它封装了Tab和Swiper两部分。

如图,tap和swipe事件有对应的swipeHandler和tapHanlder,处理流程都是先切换activeIndex,然后计算indicator的动画相关的样式(width、translateX),最后dispatch一个事件到父组件,把activeIndex传递出去。需要的参数是tabs、activeIndex、通过named slot传进来的内容,以及activeIndexChange的event handler。

Tabs组件的封装

考虑到在那个组件基础上修改可能会影响到其他的模块,且改动还是比较大的。于是我又封装了一个组件,只封装了tab的部分。

<template>
  <view class="tabs">
    <scroll-view class="tabs__navs" scroll-x="{{true}}">
      <view class="tabs__navs__wrap">
        <block wx:for="{{tabs}}" wx:for-item="tabItem" wx:key="{{index}}" >
          <view class="tabs__nav tav__nav__{{index}} {{index === activeIndex ? 'active' : ''}}"
            data-index="{{index}}" bindtap="handleTabItemTap" data-id="{{tabItem.id}}"
          >{{ tabItem.name }}</view>
        </block>
      </view>
    </scroll-view>
  </view>
</template>

<script>

export default {
  fileType: 'component',
  properties: {
    tabs: {
      type: Array,
      value: []
    },
    activeIndex: {
      type: Number,
      value: 0
    }
  },
  methods: {
    handleTabItemTap(event) {
      const activeIndex = event.target.dataset.index
      const id = event.target.dataset.id

      if(activeIndex === this.data.activeIndex) return

      this.setData({
        activeIndex
      })

      this.triggerEvent('activeIndexChange', id)
    }
  }
}
</script>

<style lang="scss">
.tabs {
  position:relative;
  margin-top:50rpx;
  margin-bottom:40rpx;
  margin-right:10rpx;
}
.tabs__nav {
  margin-right:50rpx;
  font-size:24rpx;
  line-height:44rpx;
  flex-shrink:0;
  &:first-child{
    margin-left:30rpx;
  }
  &.active {
    color:#FF5344;
    border-bottom: 4rpx solid #FF5344;
  }
}
.tabs__navs__wrap{
  position:relative;
  display:flex;
  flex-direction: row;
  flex-wrap: nowrap;
}

</style>

添加动画

做完之后,设计过来看了一下,她说分类下面的小横条要加一个动画。于是我又在之前的基础上改。

这个动画就是点击某一个分类的时候,横条会滚动到对应的tab下。也就是translateX和width会变化,width变是因为分类名的长度不一样。小程序里不能直接拿到dom元素,所以width的计算有些麻烦。我开始是直接通过fontsize乘以文字数量来算的,后来想到有英文字符这种方式算的结果就不对了,于是改用canvas的measureText来测量,后来发现比较麻烦。这时才想到有getBoundingClientRect这个api可以直接获取到,没必要自己算。
动画使用了小程序提供的animation的api,类似css的@keyframes。

<template>
  <view class="tabs">
    <scroll-view class="tabs__navs" scroll-x="{{true}}">
      <view class="tabs__navs__wrap">
        <block wx:for="{{tabs}}" wx:for-item="tabItem" wx:key="{{index}}" >
          <view class="tabs__nav tav__nav__{{index}} {{index === activeIndex ? 'active' : ''}}"
            data-index="{{index}}" bindtap="handleTabItemTap"
          >{{ tabItem }}</view>
        </block>
        <view class="activeIndicator"  animation="{{indicatorAnamationData}}"></view>
      </view>
    </scroll-view>
  </view>
</template>

<script>
import { getRect, rpx2px } from '../utils/util'

export default {
  fileType: 'component',
  properties: {
    tabs: {
      type: Array,
      value: []
    },
    activeIndex: {
      type: Number,
      value: 0
    }
  },
  data: {
    tabsLeft: [],
    tabsWidth: [],
    indicatorAnamationData: {}
  },
  ready() {
    getRect(this, '.tabs__nav', true).then(res => {
      const tabsLeft = res.map((item) => item.left * 2)
      const tabsWidth = res.map((item) => item.width * 2)
      this.setData({
        tabsLeft,
        tabsWidth
      })
    })
  },
  methods: {
    handleTabItemTap(event) {
      const activeIndex = event.target.dataset.index

      if(activeIndex === this.data.activeIndex) return

      const currentTabWidth = this.data.tabsWidth[activeIndex]
      const currentTabLeft = this.data.tabsLeft[activeIndex]
      const indicatorAnamationData = this.generateAnimationData(currentTabLeft, currentTabWidth)

      this.setData({
        activeIndex,
        indicatorAnamationData
      })

      this.triggerEvent('activeIndexChange', this.data.activeIndex)
    },
    generateAnimationData(left, width) {
      const animation = wx.createAnimation({
        duration: 500,
        timingFunction: 'ease',
      })
      const animationDatas = [
        {
          width: 10,
          left: left - this.data.tabsLeft[0]
        },
        {
          width: width,
          left: left - this.data.tabsLeft[0]
        }
      ]
      animationDatas.forEach(item => {
        const width = rpx2px(item.width)
        const left = rpx2px(item.left)
        animation.width(width).translateX(left).step()
      })
      return animation.export()
    }
  }
}
</script>

做完之后,我想着要不要给下面的内容也加上swipe的切换效果。做的过程中发现swipe的高度没法自适应,得需要自己来计算然后设置。代码改动量比较大,考虑到这个不是产品提的需求,所以决定暂时先不做了。

总结

要在已有的列表之上添加一个分类的tab,看了下已有的组件,不是很合适,需要做比较多的改动,为了避免引起其他bug,我又封装了一个Tabs组件,只把tab的部分封装了进去。之后设计说要加一个动画,分析后这段逻辑应该加在tapHanlder里切换activeIndex的之后,去修改横条的样式。但是因为横条的宽度是变化的,计算过程还是比较曲折,经历了fontsize*数量来计算,canvas的measureText最后才想到有getBoundingClientRect可用。动画的实现是基于小程序的animation api以及view组件对animation的支持。做完之后想着要不要也把下面部分的swipe切换做了,遇到高度不能自适应、对已有代码改动量大的问题,考虑到这不是产品提的需求,以及工期的原因,决定先不去研究了。

从页面的分析到基础组件的分析再到之后自己封装组件思路的梳理,流程图的作用很大,梳理清了思路之后,对于后续添加动画也很容易。于是我觉得代码和流程图应该相互补充。流程图用来分析和理清思路,代码是具体的实现和细节。

上一篇下一篇

猜你喜欢

热点阅读