搞轮子:从业务到组件Tabs
前言
因为之前为 UI 库由我来做且维护,但缺点是集成在项目中,不便于其他项目使用,且组件被同事改了之后会导致之前沿用的组件乱掉。所以 UI 由独立库维护是最好的。正逢项目不急,就将其排上了日期,其中写了
皮肤概念解决的是 UI 库目前适用于两个主色调、次色调不同的项目
css 定位解决的是 Tag 组件的 border 边框让尺寸变长问题
现在要提取 Tab 组件,发现原来的组件不太合理
业务中的 Tab 组件
业务效果如图所示,滑动它,能连带着切换下划线和内容
后端返回数据结构
总结构:
详细结构:
当时的写法:
1 | <Tab |
组件 Tab 分为三部分,主 Tab、TabItem、TabContext
在 Tab 中,集成了 TabItem 和 TabContext
TabItem:头部图片+文字
TabContext:内容卡片,支持滑动
1 | // 伪代码,此为 Tab |
又提供滑动后的回调 changeSwiper。当滑动后,意味着组件头部的位置改变,也意味着 context 数据的改变。虽然写的有点饶,但我想表达的是原来的组件的业务和逻辑绑定密切
这种写法是封装好了样式,只要传 props 即可,相当于木偶组件,使用者传入数据即可。
1 | <Tab |
缺点是子组件不能传参数,你不能控制子组件,当遇到类似的 Tab(标签页)效果,但内容结构不一致就会导致不可用。说白了,这个 Tab 只适用于当前页面的效果,没有普世性
为了组件的可拓展性,业务和项目尽量分开
重构组件
我一开始的设想是希望做成这样:
1 | <Tabs swiper={true} active="0"> |
Tabs 控制标签页是否能滑动,能否点击,滑动到那个标签页等等,Tab 控制当前标签的标题和内容
但这样,Tabs 和 Tab 就分离成了两个组件,有必要分离吗?用到标签页(Tabs)的时候必然会用 Tab,所以我后面改造成了
1 | <Tabs value="{value}"> |
Tab 嫁接在 Tabs 的子组件中
思路与做法
Tabs 的作用是为了外界的 props
Tab 的作用是展示数据,如 title 和 content
思路有了,怎么做?
Tabs 的属性包括但不限于:
value:绑定当前选中标签的标识符
swipeable:是否开启手势滑动切换
sticky:是否使用粘性定位布局
disabled:是否禁用标签
swipeThreshold:滚动阈值
…
拆开了 Tabs 和 Tab 的结构是怎么样的
1 | const Panel = (props) => { |
这里有个知识点:React.Children
什么是 React.Children?
React.Children
提供了用于处理this.props.children
不透明数据结构的实用方法。
React.Children.map 的使用方法
1 | React.Children.map(children, function[(thisArg)]) |
所以在代码中
1 | React.Children.map(children, (item, index) => <TabPanel {...item.props} />) |
(item, index) => <TabPanel />
表示的是 children 中的每个 Tabs.Panel
联想到组件
1 | <Tabs value={value}> |
在这个案例中,React.Children 表示 children,即三个 Tabs.Panel
,并且将其遍历
(item, index) => <TabPanel />
,就一一对应每个 TabPanel
所以 children 中的内容必须是 Tans.Panel
组件,这样才不会出差错
同理,我们可以把标题抽离出来,成一”木偶组件”,React.Children.map
时传入所需要的数据
这里需要注意的是,Tabs.Panel
组件承担了 Tabs.Title
传值的作用,其 title、img 等都是传给 Tabs.Title
的参数。
做组件需要注意什么
它的结构
- 我对 标签页组件是拆分为
Tabs
、Tabs.Panel
、Tabs.Title
。其中Tabs.Title
没有暴露出来,因为不需要
- 我对 标签页组件是拆分为
它有什么属性
- swipeable:支持滑动
- 如果需要滑动,那么就需要用到
Swiper
滑块组件 - 当它滑动时,标题中的 line 也要跟着滚动到相应的位置
- 如果需要滑动,那么就需要用到
- sticky:粘性布局
- 同样,粘性布局在很多场景下都有用到,是否需要抽离成组件
- swipeThreshold:滚动阈值
- 当 children 中的数量(React.Children.Count)大于阈值时支持横向滚动
- swipeable:支持滑动
它的下标跟随怎么做
首先要先获取每个
Tabs.Title
的 ref,即获取它的 dom- 做
Tabs.Title
时转发(forwardRef) 此组件 - 使用
<Title ref={(el) => (tabsTitleRef.current[index] = el)} ... />
获取每个Tabs.Title
的 dom
- 做
补间动画
- 拿到点击后的值的 dom 的 坐标,
transform: translateX(XX)
- 拿到点击后的值的 dom 的 坐标,
当达到滚动阈值后,
tabsNavRef
(Tabs.Title
的父容器) 移动,移动是有公式的function scrollLeftTo() { cancelRaf(rafId) let count = 0 const from = scroller.scrollLeft const frames = duration === 0 ? 1 : Math.round((duration * 1000) / 16) function animate() { scroller.scrollLeft += (to - from) / frames if (++count < frames) { rafId = raf(animate) } } animate() }
其余的没什么难度了
总结
Tabs 标签页组件的核心在于什么?标签的标题和内容,点击标签时,内容切换到该页面以及当它可以滑动时,变化的是当前的下标。我们需要以它为切入点,当它变化时,下划线滚动。而点击 Title 或者滑动 Content 都会触发 setCurrentIndex(index)
,用 hooks 写完美解决”关注点问题“