流程图与代码:一个Tabs组件的封装过程
需求描述
最近在做这样一个需求:在商品列表之上加一个分类,点击分类可以切换商品。
交互分析
商品列表已经有了,需要做的只是上面的分类的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切换做了,遇到高度不能自适应、对已有代码改动量大的问题,考虑到这不是产品提的需求,以及工期的原因,决定先不去研究了。
从页面的分析到基础组件的分析再到之后自己封装组件思路的梳理,流程图的作用很大,梳理清了思路之后,对于后续添加动画也很容易。于是我觉得代码和流程图应该相互补充。流程图用来分析和理清思路,代码是具体的实现和细节。