搞轮子:从业务到组件Tabs
前言
因为之前为 UI 库由我来做且维护,但缺点是集成在项目中,不便于其他项目使用,且组件被同事改了之后会导致之前沿用的组件乱掉。所以 UI 由独立库维护是最好的。正逢项目不急,就将其排上了日期,其中写了
皮肤概念解决的是 UI 库目前适用于两个主色调、次色调不同的项目
css 定位解决的是 Tag 组件的 border 边框让尺寸变长问题
现在要提取 Tab 组件,发现原来的组件不太合理
业务中的 Tab 组件
业务效果如图所示,滑动它,能连带着切换下划线和内容
后端返回数据结构
总结构:
详细结构:
当时的写法:
<Tab
ref={tabRef}
data={tagGroupProductList}
sticky={true}
selected={selectedTab}
onChange={onHandleChangeTab}
onClick={onClickToBtn}
/>
组件 Tab 分为三部分,主 Tab、TabItem、TabContext
在 Tab 中,集成了 TabItem 和 TabContext
TabItem:头部图片+文字
TabContext:内容卡片,支持滑动
// 伪代码,此为 Tab
<div className="jing-tab" style={style} ref={ref}>
// 遍历<TabItem></TabItem>
// 塞入当前下标页的数据,展示 TabContext <TabContext />
</div>
又提供滑动后的回调 changeSwiper。当滑动后,意味着组件头部的位置改变,也意味着 context 数据的改变。虽然写的有点饶,但我想表达的是原来的组件的业务和逻辑绑定密切
这种写法是封装好了样式,只要传 props 即可,相当于木偶组件,使用者传入数据即可。
<Tab
ref={tabRef}
data={tagGroupProductList} // 传入一次性参数
sticky={true}
selected={selectedTab}
onChange={onHandleChangeTab}
onClick={onClickToBtn}
/>
缺点是子组件不能传参数,你不能控制子组件,当遇到类似的 Tab(标签页)效果,但内容结构不一致就会导致不可用。说白了,这个 Tab 只适用于当前页面的效果,没有普世性
为了组件的可拓展性,业务和项目尽量分开
重构组件
我一开始的设想是希望做成这样:
<Tabs swiper={true} active="0">
<Tab title="标签1">内容1</Tab>
<Tab title="标签2">内容2</Tab>
<Tab title="标签3">内容3</Tab>
</Tabs>
Tabs 控制标签页是否能滑动,能否点击,滑动到那个标签页等等,Tab 控制当前标签的标题和内容
但这样,Tabs 和 Tab 就分离成了两个组件,有必要分离吗?用到标签页(Tabs)的时候必然会用 Tab,所以我后面改造成了
<Tabs value="{value}">
<Tabs.Panel title="标签1">内容 1</Tabs.Panel>
<Tabs.Panel title="标签2">内容 2</Tabs.Panel>
<Tabs.Panel title="标签3">内容 3</Tabs.Panel>
</Tabs>
Tab 嫁接在 Tabs 的子组件中
思路与做法
Tabs 的作用是为了外界的 props
Tab 的作用是展示数据,如 title 和 content
思路有了,怎么做?
Tabs 的属性包括但不限于:
-
value:绑定当前选中标签的标识符
-
swipeable:是否开启手势滑动切换
-
sticky:是否使用粘性定位布局
-
disabled:是否禁用标签
-
swipeThreshold:滚动阈值
-
…
拆开了 Tabs 和 Tab 的结构是怎么样的
const Panel = (props) => {
const classes = classnames(prefixCls, props.className, {
[`${prefixCls}--active`]: !!props.selected,
[`${prefixCls}--disabled`]: !!props.disabled,
});
return (
<div className="jing-tabs__panel" style={props.style} role="tabpanel">
{props.children}
</div>
)
}
const Tabs = (props) => {
return (
<div className="jing-tabs">
<div className="jing-tabs__wrap">
React.Children.map(children, (item: any, index: number) => {
// 简化
return (
<Title ...item.props/>
);
})
</div>
<div className="jing-tabs__content">
// 遍历 React.Children,将Children中的数据塞入 TabPanel 中
React.Children.map(children, (item,index) => (
<TabPanel {...item.props}/>
))
</div>
</div>
)
}
Tabs.Panel = Panel
这里有个知识点:React.Children
什么是 React.Children?
React.Children
提供了用于处理this.props.children
不透明数据结构的实用方法。
React.Children.map 的使用方法
React.Children.map(children, function[(thisArg)])
所以在代码中
React.Children.map(children, (item, index) => <TabPanel {...item.props} />);
(item, index) => <TabPanel />
表示的是 children 中的每个 Tabs.Panel
联想到组件
<Tabs value={value}>
<Tabs.Panel title="标签1">内容 1</Tabs.Panel>
<Tabs.Panel title="标签2">内容 2</Tabs.Panel>
<Tabs.Panel title="标签3">内容 3</Tabs.Panel>
</Tabs>
在这个案例中,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 写完美解决”关注点问题“